mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-20 14:52:22 +02:00
web ui
This commit is contained in:
@@ -25,7 +25,7 @@ button {
|
||||
//}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
.tp__hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ pub struct CompiledRegex {
|
||||
pub restr: String,
|
||||
pub re: regex::Regex,
|
||||
}
|
||||
impl PartialEq for CompiledRegex {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.restr == other.restr
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar_inline = r#"
|
||||
@@ -105,12 +110,12 @@ main = _{ SOI ~ stmt ~ EOI }
|
||||
"#]
|
||||
struct FilterParser;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum UnaryOperator {
|
||||
Not,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum BinaryOperator {
|
||||
And,
|
||||
Or,
|
||||
@@ -130,7 +135,7 @@ impl std::fmt::Display for BinaryOperator {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Filter {
|
||||
Group(Box<Filter>),
|
||||
FieldComparison(ItemField, CompiledRegex),
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKin
|
||||
use crate::foundation::filter::apply_templates_to_pattern_single;
|
||||
use crate::model::{ItemField, PatternTemplate};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigRenameDto {
|
||||
pub field: ItemField,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use regex::Regex;
|
||||
use crate::{create_tuliprox_error, handle_tuliprox_error_result_list};
|
||||
use crate::error::{TuliproxError, TuliproxErrorKind};
|
||||
use crate::foundation::filter::{apply_templates_to_pattern, apply_templates_to_pattern_single};
|
||||
use crate::model::{ItemField, PatternTemplate, TemplateValue};
|
||||
use crate::{create_tuliprox_error, handle_tuliprox_error_result_list};
|
||||
use regex::Regex;
|
||||
|
||||
fn compile_regex_vec(patterns: Option<&Vec<String>>) -> Result<Option<Vec<Regex>>, TuliproxError> {
|
||||
patterns.as_ref()
|
||||
@@ -16,7 +16,7 @@ fn compile_regex_vec(patterns: Option<&Vec<String>>) -> Result<Option<Vec<Regex>
|
||||
.transpose() // convert Option<Result<...>> to Result<Option<...>>
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub enum SortOrder {
|
||||
#[serde(rename = "asc")]
|
||||
Asc,
|
||||
@@ -34,8 +34,14 @@ pub struct ConfigSortGroupDto {
|
||||
pub t_sequence: Option<Vec<Regex>>,
|
||||
}
|
||||
|
||||
impl ConfigSortGroupDto {
|
||||
impl PartialEq for ConfigSortGroupDto {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.order == other.order
|
||||
&& self.sequence == other.sequence
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigSortGroupDto {
|
||||
pub fn prepare(&mut self, templates: Option<&Vec<PatternTemplate>>) -> Result<(), TuliproxError> {
|
||||
let processed_sequence = match (&self.sequence, templates) {
|
||||
(Some(seqs), Some(_templs)) => {
|
||||
@@ -47,7 +53,7 @@ impl ConfigSortGroupDto {
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
},
|
||||
}
|
||||
(Some(seqs), None) => Some(seqs.clone()),
|
||||
(None, _) => None,
|
||||
};
|
||||
@@ -71,6 +77,16 @@ pub struct ConfigSortChannelDto {
|
||||
pub t_sequence: Option<Vec<Regex>>,
|
||||
}
|
||||
|
||||
impl PartialEq for ConfigSortChannelDto {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.field == other.field
|
||||
&& self.group_pattern == other.group_pattern
|
||||
&& self.order == other.order
|
||||
&& self.sequence == other.sequence
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ConfigSortChannelDto {
|
||||
pub fn prepare(&mut self, templates: Option<&Vec<PatternTemplate>>) -> Result<(), TuliproxError> {
|
||||
self.group_pattern = apply_templates_to_pattern_single(&self.group_pattern, templates)?;
|
||||
@@ -90,7 +106,7 @@ impl ConfigSortChannelDto {
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
},
|
||||
}
|
||||
(Some(seqs), None) => Some(seqs.clone()),
|
||||
(None, _) => None,
|
||||
};
|
||||
@@ -101,7 +117,7 @@ impl ConfigSortChannelDto {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigSortDto {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{create_tuliprox_error_result, handle_tuliprox_error_result_list, inf
|
||||
use crate::foundation::filter::{get_filter, Filter};
|
||||
use crate::model::{ClusterFlags, ConfigRenameDto, ConfigSortDto, PatternTemplate, ProcessingOrder, StrmExportStyle, TargetType, TraktConfigDto};
|
||||
use crate::utils::{default_as_true, default_resolve_delay_secs, default_as_default};
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigTargetOptions {
|
||||
#[serde(default)]
|
||||
@@ -17,7 +17,7 @@ pub struct ConfigTargetOptions {
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct XtreamTargetOutputDto {
|
||||
#[serde(default = "default_as_true")]
|
||||
@@ -46,7 +46,7 @@ impl XtreamTargetOutputDto {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct M3uTargetOutputDto {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -57,7 +57,7 @@ pub struct M3uTargetOutputDto {
|
||||
pub mask_redirect_url: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StrmTargetOutputDto {
|
||||
pub directory: String,
|
||||
@@ -74,7 +74,7 @@ pub struct StrmTargetOutputDto {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub strm_props: Option<Vec<String>>,
|
||||
}
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HdHomeRunTargetOutputDto {
|
||||
pub device: String,
|
||||
@@ -83,7 +83,7 @@ pub struct HdHomeRunTargetOutputDto {
|
||||
pub use_output: Option<TargetType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")]
|
||||
pub enum TargetOutputDto {
|
||||
Xtream(XtreamTargetOutputDto),
|
||||
@@ -103,7 +103,7 @@ impl TargetOutputDto {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigTargetDto {
|
||||
#[serde(skip)]
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Default for TraktContentType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TraktApiConfigDto {
|
||||
#[serde(default)]
|
||||
@@ -44,7 +44,7 @@ impl TraktApiConfigDto {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TraktListConfigDto {
|
||||
pub user: String,
|
||||
@@ -55,7 +55,7 @@ pub struct TraktListConfigDto {
|
||||
pub fuzzy_match_threshold: u8, // Percentage (0-100)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TraktConfigDto {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -107,8 +107,8 @@
|
||||
"BUILD_TIME": "Build time",
|
||||
"MEMORY": "Memory",
|
||||
"ACTIVE_USERS": "Active users",
|
||||
"ACTIVE_USER_CONNECTIONS": "Active user conn...",
|
||||
"ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn...",
|
||||
"ACTIVE_USER_CONNECTIONS": "Active user connections",
|
||||
"ACTIVE_PROVIDER_CONNECTIONS": "Active provider connections",
|
||||
"IP_CHECK": "IP Check",
|
||||
"URL_IPV4": "Url IPv4",
|
||||
"URL_IPV6": "Url IPv6",
|
||||
@@ -130,7 +130,25 @@
|
||||
"DOCUMENTATION": "Documentation",
|
||||
"DOCUMENTATION_CONTENT": "Get started with tuliprox: features, setup, and examples",
|
||||
"OPEN_DOCUMENTATION": "Documentation",
|
||||
"NOT_ACTIVATED": "Feature is not activated"
|
||||
"NOT_ACTIVATED": "Feature is not activated",
|
||||
"CREATE": "Create",
|
||||
"PLAYLIST": "Playlist",
|
||||
"PLAYLISTS": "Playlists",
|
||||
"LIST": "List",
|
||||
"NEW_PLAYLIST": "New Playlist"
|
||||
},
|
||||
"TABLE": {
|
||||
"ID": "Id",
|
||||
"ENABLED": "Enabled",
|
||||
"NAME": "Name",
|
||||
"OPTIONS": "Options",
|
||||
"SORT": "Sort",
|
||||
"FILTER": "Filter",
|
||||
"OUTPUT": "Output",
|
||||
"RENAME": "Rename",
|
||||
"MAPPING": "Mapping",
|
||||
"PROCESSING_ORDER": "Processing order",
|
||||
"WATCH": "Watch"
|
||||
},
|
||||
"TITLE": {
|
||||
"USER_BOUQUET_EDITOR": "User group editor"
|
||||
|
||||
@@ -89,6 +89,12 @@
|
||||
],
|
||||
"path": "M8 5v14l11-7z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"PlayArrowOutline"
|
||||
],
|
||||
"path": "m 6.9091055,7.1999977 7.6654235,4.7999983 -7.6654235,4.8 z M 4,2 V 22 L 20,11.999996 Z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"ArrowLeft"
|
||||
@@ -185,6 +191,12 @@
|
||||
],
|
||||
"path": "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"UserOutline"
|
||||
],
|
||||
"path": "m 12,4.5 c 1.375,0 2.5,1.125 2.5,2.5 0,1.375 -1.125,2.5 -2.5,2.5 C 10.625,9.5 9.5,8.375 9.5,7 9.5,5.625 10.625,4.5 12,4.5 M 12,17 c 3.375,0 7.25,1.6125 7.5,2.5 H 4.5 C 4.7875,18.6 8.6375,17 12,17 M 12,2 C 9.2375,2 7,4.2375 7,7 c 0,2.7625 2.2375,5 5,5 2.7625,0 5,-2.2375 5,-5 0,-2.7625 -2.2375,-5 -5,-5 m 0,12.5 c -3.3375,0 -10,1.675 -10,5 V 22 h 20 v -2.5 c 0,-3.325 -6.6625,-5 -10,-5"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"Refresh"
|
||||
@@ -299,6 +311,10 @@
|
||||
"keys": ["Dashboard"],
|
||||
"path": "M11 21H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h6zm2 0h6c1.1 0 2-.9 2-2v-7h-8zm8-11V5c0-1.1-.9-2-2-2h-6v7z"
|
||||
},
|
||||
{
|
||||
"keys": ["DashboardOutline"],
|
||||
"path": "M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M5 19V5h6v14zm14 0h-6v-7h6zm0-9h-6V5h6z"
|
||||
},
|
||||
{
|
||||
"keys": ["Settings"],
|
||||
"path": "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6"
|
||||
@@ -318,5 +334,13 @@
|
||||
{
|
||||
"keys": ["Router", "Network"],
|
||||
"path": "m20.2 5.9.8-.8C19.6 3.7 17.8 3 16 3s-3.6.7-5 2.1l.8.8C13 4.8 14.5 4.2 16 4.2s3 .6 4.2 1.7m-.9.8c-.9-.9-2.1-1.4-3.3-1.4s-2.4.5-3.3 1.4l.8.8c.7-.7 1.6-1 2.5-1s1.8.3 2.5 1zM19 13h-2V9h-2v4H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2M8 18H6v-2h2zm3.5 0h-2v-2h2zm3.5 0h-2v-2h2z"
|
||||
},
|
||||
{
|
||||
"keys": ["PlaylistAdd"],
|
||||
"path": "M 13.578946,8.2857137 H 2 V 10.428572 H 13.578946 Z M 13.578946,4 H 2 v 2.1428548 h 11.578946 z m 4.210508,8.571428 V 8.2857137 h -2.105235 v 4.2857143 h -4.210547 v 2.142859 h 4.210547 V 19 h 2.105235 V 14.714287 H 22 V 12.571428 Z M 2,14.714287 H 9.368438 V 12.571428 H 2 Z"
|
||||
},
|
||||
{
|
||||
"keys": ["Playlist"],
|
||||
"path": "M 2,8.2666668 H 13.578948 V 10.4 H 2 Z M 2,4 H 13.578948 V 6.1333336 H 2 Z m 0,8.533334 h 7.3684211 v 2.133333 H 2 Z M 15.684211,11.466667 V 20 L 22,15.733334 Z"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
.tp__action-card {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--gap-default);
|
||||
fill: var(--card-color);
|
||||
|
||||
&__icon {
|
||||
fill: inherit;
|
||||
width: 3rem;
|
||||
transform: translateY(-0.5rem);
|
||||
|
||||
img {
|
||||
width: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
@use 'common';
|
||||
|
||||
.app {
|
||||
.tp__app {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
overflow: hidden;
|
||||
@@ -16,6 +14,8 @@
|
||||
flex: 1 1 auto;
|
||||
align-content: flex-start;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
.app-header {
|
||||
.tp__app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -16,7 +15,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-small);
|
||||
.icon-button {
|
||||
.tp__icon-button {
|
||||
width: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
.tp__breadcrumbs {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
ol {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tp__breadcrumbs__icon {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
fill: var(--modest-text-color);
|
||||
svg {
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
box-sizing: border-box;
|
||||
color: var(--modest-text-color);
|
||||
|
||||
}
|
||||
|
||||
&__active {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__selectable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
.card {
|
||||
.tp__card {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: var(--card-background-color);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.collapse-panel {
|
||||
.tp__collapse-panel {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8px;
|
||||
|
||||
+3
-102
@@ -1,105 +1,6 @@
|
||||
$media-size-mobile: 600px;
|
||||
|
||||
@mixin container_common($justify-content: flex-start, $padding: 12px) {
|
||||
display: flex;
|
||||
justify-content: $justify-content;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
@media (max-width: $media-size-mobile) {
|
||||
padding: $padding;
|
||||
}
|
||||
@media (min-width: $media-size-mobile) {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin container_vert() {
|
||||
flex-flow: column;
|
||||
@include container_common();
|
||||
}
|
||||
|
||||
@mixin container_horz() {
|
||||
flex-flow: row;
|
||||
@include container_common();
|
||||
}
|
||||
|
||||
@mixin container_horz_nowrap($justify-content: flex-start, $padding: 12px) {
|
||||
flex-flow: row nowrap;
|
||||
@media (max-width: $media-size-mobile) {
|
||||
flex-flow: column;
|
||||
}
|
||||
@include container_common($justify-content, $padding);
|
||||
}
|
||||
|
||||
@mixin container_horz_no_padding() {
|
||||
flex-flow: row;
|
||||
@include container_common($padding: 0);
|
||||
}
|
||||
|
||||
@mixin container_vert_no_padding($justify-content: flex-start) {
|
||||
flex-flow: column;
|
||||
@include container_common($justify-content, $padding: 0);
|
||||
}
|
||||
|
||||
@mixin activeOptionButton {
|
||||
box-sizing: border-box !important;
|
||||
background-color: var(--option-active-background-color) !important;
|
||||
color: var(--option-active-color) !important;
|
||||
font-weight: bold;
|
||||
svg {
|
||||
color: var(--option-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin textButton {
|
||||
border: 1px solid var(--text-button-border-color);
|
||||
font-size: 1.2rem;
|
||||
border-radius: var(--text-button-border-radius);
|
||||
padding: 8px 16px;
|
||||
background-color: var(--text-button-background-color);
|
||||
color: var(--text-button-color);
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: var(--text-button-hover-background-color);
|
||||
color: var(--text-button-hover-color);
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin list {
|
||||
ul {
|
||||
list-style-type: none; /* Remove bullets */
|
||||
padding: 0; /* Remove padding */
|
||||
margin: 0; /* Remove margins */
|
||||
li {
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin list-hover {
|
||||
ul {
|
||||
list-style-type: none; /* Remove bullets */
|
||||
padding: 0; /* Remove padding */
|
||||
margin: 0; /* Remove margins */
|
||||
li {
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--popup-hover-color);
|
||||
background-color: var(--popup-hover-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes pulse-bg {
|
||||
@keyframes tp__pulse-bg {
|
||||
0% {
|
||||
background-color: var(--pulse-color-1);
|
||||
}
|
||||
@@ -111,6 +12,6 @@ $media-size-mobile: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse-bg 500ms ease-out;
|
||||
.tp__pulse {
|
||||
animation: tp__pulse-bg 500ms ease-out;
|
||||
}
|
||||
@@ -2,12 +2,19 @@
|
||||
@forward "menu_item";
|
||||
@forward "icon_button";
|
||||
@forward "text_button";
|
||||
@forward "toggle_switch";
|
||||
@forward "table";
|
||||
@forward "app_header";
|
||||
@forward "app";
|
||||
@forward "panel";
|
||||
@forward "card";
|
||||
@forward "action_card";
|
||||
@forward "status_card";
|
||||
@forward "collapse_panel";
|
||||
@forward "breadcrumbs";
|
||||
@forward "login";
|
||||
@forward "sidebar";
|
||||
@forward "preferences";
|
||||
@forward "dashboard_view";
|
||||
@forward "playlist_view";
|
||||
@forward "playlist_assistant";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
$dashboard-mobile-size: 1280px;
|
||||
|
||||
.dashboard {
|
||||
.tp__dashboard {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
@@ -39,64 +39,11 @@ $dashboard-mobile-size: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--gap-default);
|
||||
fill: var(--card-color);
|
||||
|
||||
&__icon {
|
||||
fill: inherit;
|
||||
width: 3rem;
|
||||
transform: translateY(-0.5rem);
|
||||
|
||||
img {
|
||||
width: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-default);
|
||||
&__title {
|
||||
color: var(--modest-text-color);
|
||||
}
|
||||
&__body {
|
||||
color: var(--text-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
&__footer {
|
||||
color: var(--modest-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.discord {
|
||||
.tp__discord {
|
||||
fill: var(--discord-color);
|
||||
}
|
||||
|
||||
.documentation {
|
||||
.tp__documentation {
|
||||
fill: var(--documenation-color);
|
||||
|
||||
svg {
|
||||
|
||||
@@ -21,47 +21,13 @@ button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
.tp__hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vert-group {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
padding: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
//
|
||||
//.tool-button {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// height: 1.1rem;
|
||||
// width: 1.1rem;
|
||||
// border-radius: var(--border-radius);
|
||||
// background-color: var(--tool-button-background-color);
|
||||
// color: var(--tool-button-color);
|
||||
// cursor: pointer;
|
||||
//
|
||||
// svg {
|
||||
// width: 1rem;
|
||||
// height: 1rem;
|
||||
// }
|
||||
//
|
||||
// &:hover {
|
||||
// color: var(--tool-button-hover-color);
|
||||
// background-color: var(--tool-button-hover-background-color);
|
||||
// }
|
||||
//
|
||||
// &:active {
|
||||
// transform: scale(0.9);
|
||||
// }
|
||||
//}
|
||||
|
||||
:not(input[type="text"]) {
|
||||
caret-color: transparent;
|
||||
@@ -69,18 +35,6 @@ select {
|
||||
input[type="text"] {
|
||||
caret-color: auto;
|
||||
}
|
||||
//
|
||||
//select {
|
||||
// height: 2rem;
|
||||
// border-radius: var(--border-radius);
|
||||
// font-size: 1.2rem;
|
||||
// padding: 0 8px;
|
||||
// cursor: pointer;
|
||||
// &:hover {
|
||||
// color: var(--text-button-hover-color);
|
||||
// background-color: var(--text-button-hover-background-color);
|
||||
// }
|
||||
//}
|
||||
|
||||
option {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.icon-button {
|
||||
.tp__icon-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.input {
|
||||
.tp__input {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use 'common';
|
||||
|
||||
.login-view {
|
||||
.tp__login-view {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.menu-item {
|
||||
.tp__menu-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
.panel {
|
||||
.tp__panel {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
padding: 60px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--card-background-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.tp__playlist-assistant {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
.tp__playlist-view {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
padding: var(--padding-default) 0;
|
||||
gap: var(--gap-default);
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tp__playlist-create,
|
||||
.tp__playlist-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
gap: var(--gap-default);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,24 @@
|
||||
@use 'common';
|
||||
|
||||
@mixin prefsRoot {
|
||||
@include common.container_vert_no_padding(flex-start);
|
||||
align-content: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@mixin prefsInput {
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
padding: 0.2rem 0.5rem;
|
||||
flex: 1 1 auto;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin prefsToolbar() {
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--gap-default);
|
||||
background-color: var(--toolbar-bg-color);
|
||||
padding: 8px 16px;
|
||||
|
||||
label {
|
||||
flex: 1 1 auto;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--title-label-color);
|
||||
}
|
||||
|
||||
button {
|
||||
@include common.textButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin prefsForm() {
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
&-help {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap:12px;
|
||||
border-top: 1px solid var(--border-light-color);
|
||||
padding-top: 12px;
|
||||
color: var(--label-color);
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.preferences {
|
||||
@include common.container_horz_no_padding();
|
||||
.tp__preferences {
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&__content {
|
||||
@include common.container_horz_nowrap(flex-start, 0);
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&__panels {
|
||||
@include common.container_vert_no_padding(flex-start);
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.panel {
|
||||
@include common.container_vert();
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
.card {
|
||||
@include common.container_vert();
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
$sidebar-width: 270px;
|
||||
|
||||
.app-sidebar {
|
||||
.tp__app-sidebar {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-default);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.tp__status-card {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-default);
|
||||
&__title {
|
||||
color: var(--modest-text-color);
|
||||
}
|
||||
&__body {
|
||||
color: var(--text-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
&__footer {
|
||||
color: var(--modest-text-color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
.tp__table {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-default);
|
||||
padding: var(--padding-default) 0;
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0.875rem;
|
||||
background-color: var(--table-header-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
.text-button {
|
||||
.tp__text-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -32,12 +32,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.text-button.button-primary {
|
||||
.tp__text-button.tp__button-primary {
|
||||
background-color: var(--text-button-primary-background-color);
|
||||
color: var(--text-button-primary-color);
|
||||
fill : var(--text-button-primary-color);
|
||||
&:hover {
|
||||
background-color: var(--text-button-primary-hover-background-color);
|
||||
color: var(--text-button-primary-hover-color);
|
||||
fill : var(--text-button-primary-hover-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -8,6 +8,8 @@
|
||||
--padding-default: 1.5rem;
|
||||
--padding-large: 2rem;
|
||||
|
||||
--primary-color: #124a9b;
|
||||
|
||||
--action-item-background-color: #242427;
|
||||
|
||||
--text-color: #ffffff;
|
||||
@@ -17,12 +19,12 @@
|
||||
--input-border-color: #454548;
|
||||
--input-focus-border-color: #12499a;
|
||||
--input-autofill-background-color: #323a48;
|
||||
--button-background-color: #124a9b;
|
||||
--button-background-color: var(--primary-color);
|
||||
--border-color: #222224;
|
||||
--menu-text-color: #e2e2e5;
|
||||
--menu-icon-color: #707079;
|
||||
--menu-item-hover-background-color: var(--action-item-background-color);
|
||||
--menu-text-active-color: #124a9b;
|
||||
--menu-text-active-color: var(--primary-color);
|
||||
--menu-item-active-background-color: var(--menu-item-hover-background-color);
|
||||
--icon-button-hover-color: #8f8f97;
|
||||
--card-background-color: #18181b;
|
||||
@@ -32,11 +34,17 @@
|
||||
--text-button-color: #ffffff;
|
||||
--text-button-border-color: #2F2F31;
|
||||
--text-button-hover-color: #303032;
|
||||
--text-button-primary-background-color: #124a9b;
|
||||
--text-button-primary-background-color: var(--primary-color);
|
||||
--text-button-primary-color: #ffffff;
|
||||
--text-button-primary-hover-background-color: #2f68be;
|
||||
--text-button-primary-hover-color: #ffffff;
|
||||
|
||||
--toggle-switch-on-background-color: var(--primary-color);
|
||||
--toggle-switch-off-background-color: #888888;
|
||||
--toggle-switch-toggle-color: #ffffff;
|
||||
|
||||
--table-header-background-color: #242427;
|
||||
|
||||
--discord-color: #5764f0;
|
||||
--documenation-color: #2b944b;
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
$toggle-switch-width: 3.2rem;
|
||||
$toggle-switch-height: 1.6rem;
|
||||
$toggle-switch-toggle-padding: 3px;
|
||||
$toggle-switch-toggle-width: calc($toggle-switch-height - $toggle-switch-toggle-padding * 2);
|
||||
$toggle-switch-toggle-end-pos: calc($toggle-switch-width - ($toggle-switch-toggle-width + $toggle-switch-toggle-padding * 2));
|
||||
|
||||
.tp__toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: $toggle-switch-width;
|
||||
height: $toggle-switch-height;
|
||||
input {
|
||||
opacity: 0; width: 0; height: 0
|
||||
}
|
||||
input:checked + span {
|
||||
background-color: var(--toggle-switch-on-background-color);
|
||||
}
|
||||
input:checked + span > span {
|
||||
transform: translateX($toggle-switch-toggle-end-pos);
|
||||
}
|
||||
&__track {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: var(--toggle-switch-off-background-color);
|
||||
border-radius: $toggle-switch-toggle-width;
|
||||
transition: 0.3s;
|
||||
|
||||
}
|
||||
&__toggle {
|
||||
position: absolute;
|
||||
height: $toggle-switch-toggle-width;
|
||||
width: $toggle-switch-toggle-width;
|
||||
left: $toggle-switch-toggle-padding;
|
||||
bottom: $toggle-switch-toggle-padding;
|
||||
background-color: var(--toggle-switch-toggle-color);
|
||||
border-radius: 50%;
|
||||
transition: 0.3s;
|
||||
transform: translateX(0);
|
||||
}
|
||||
&__active {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use wasm_bindgen::JsCast;
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::AppIcon;
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct BreadcrumbsProps {
|
||||
pub items: Rc<Vec<String>>,
|
||||
#[prop_or_default]
|
||||
pub onclick: Callback<(String, usize)>,
|
||||
}
|
||||
#[function_component]
|
||||
pub fn Breadcrumbs(props: &BreadcrumbsProps) -> Html {
|
||||
let len = props.items.len();
|
||||
|
||||
let handle_click = {
|
||||
let click = props.onclick.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.prevent_default();
|
||||
if let Some(target) = e.target() {
|
||||
if let Ok(element) = target.dyn_into::<web_sys::Element>() {
|
||||
if let Some(data_name) = element.get_attribute("data-name") {
|
||||
if let Some(data_index) = element.get_attribute("data-index") {
|
||||
if let Ok(index) = usize::from_str(&data_index) {
|
||||
click.emit((data_name, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<nav class="tp__breadcrumbs" aria-label="Breadcrumb">
|
||||
<ol>
|
||||
{ for props.items.iter().enumerate().map(|(i, item)| {
|
||||
html! {
|
||||
<li>
|
||||
{
|
||||
if i > 0 {
|
||||
html! { <span class="tp__breadcrumbs__icon"><AppIcon name="ChevronRight"/></span> }
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
{
|
||||
if i < len -1 {
|
||||
html! { <span class="tp__breadcrumbs__selectable"
|
||||
data-index={i.to_string()}
|
||||
data-name={ item.to_string() }
|
||||
onclick={ handle_click.clone() }>{ &item }</span>
|
||||
}
|
||||
} else {
|
||||
html! { <span class="tp__breadcrumbs__active">{ &item }</span> }
|
||||
}
|
||||
}
|
||||
</li>
|
||||
}
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ pub fn Card(props: &CardProps) -> Html {
|
||||
};
|
||||
html! {
|
||||
<ContextProvider<CardContext> context={context}>
|
||||
<div class={classes!("card", &*custom_class)}>
|
||||
<div class={classes!("tp__card", &*custom_class)}>
|
||||
{ for props.children.iter() }
|
||||
</div>
|
||||
</ContextProvider<CardContext>>
|
||||
|
||||
@@ -19,13 +19,13 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="collapse-panel">
|
||||
<div class="collapse-panel__header" onclick={toggle}>
|
||||
<div class="tp__collapse-panel">
|
||||
<div class="tp__collapse-panel__header" onclick={toggle}>
|
||||
<span>{ props.title.clone() }</span>
|
||||
<AppIcon name={ if *expanded { "ChevronUp" } else {"ChevronDown"} }/>
|
||||
</div>
|
||||
if *expanded {
|
||||
<div class="collapse-panel__body">
|
||||
<div class="tp__collapse-panel__body">
|
||||
{ for props.children.iter() }
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ pub struct ActionProps {
|
||||
pub fn ActionCard(props: &ActionProps) -> Html {
|
||||
|
||||
html! {
|
||||
<div class={if props.classname.is_empty() {"action-card".to_string()} else {format!("action-card {}", props.classname)}}>
|
||||
<div class="action-card__icon">
|
||||
<div class={classes!("tp__action-card", if props.classname.is_empty() {String::new()} else {props.classname.to_string()})}>
|
||||
<div class="tp__action-card__icon">
|
||||
<AppIcon name={props.icon.clone()} />
|
||||
</div>
|
||||
<div class="action-card__body">
|
||||
<span class="action-card__title">
|
||||
<div class="tp__action-card__body">
|
||||
<span class="tp__action-card__title">
|
||||
{props.title.clone()}
|
||||
</span>
|
||||
<span class="action-card__content">
|
||||
<span class="tp__action-card__content">
|
||||
{ props.subtitle.clone() }
|
||||
{
|
||||
if !props.subtitle_html.is_empty() {
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn DiscordActionCard() -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<ActionCard icon="Discord" classname="discord" title={translate.t("LABEL.DISCORD")}
|
||||
<ActionCard icon="Discord" classname="tp__discord" title={translate.t("LABEL.DISCORD")}
|
||||
subtitle={translate.t("LABEL.JOIN_ON_DISCORD")}>
|
||||
<TextButton name="discord" title={translate.t("LABEL.OPEN_DISCORD")} icon="Link" onclick={handle_url} />
|
||||
</ActionCard>
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn DocumentationActionCard() -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<ActionCard icon="Book" classname="documentation" title={translate.t("LABEL.DOCUMENTATION")}
|
||||
<ActionCard icon="Book" classname="tp__documentation" title={translate.t("LABEL.DOCUMENTATION")}
|
||||
subtitle={translate.t("LABEL.DOCUMENTATION_CONTENT")}>
|
||||
<TextButton name="documentation" title={translate.t("LABEL.OPEN_DOCUMENTATION")} icon="Link" onclick={handle_url} />
|
||||
</ActionCard>
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn IpinfoActionCard() -> Html {
|
||||
if exists {
|
||||
let card_ctx_2 = card_ctx.clone();
|
||||
if let Some(card_context) = card_ctx_1.as_ref() {
|
||||
card_context.custom_class.set("pulse".to_string());
|
||||
card_context.custom_class.set("tp__pulse".to_string());
|
||||
}
|
||||
let services_ctx = services_ctx.clone();
|
||||
let ip_address_state = ip_address_state.clone();
|
||||
@@ -87,7 +87,7 @@ pub fn IpinfoActionCard() -> Html {
|
||||
}
|
||||
|
||||
html! {
|
||||
<ActionCard icon="Network" classname="ipinfo" title={translate.t("LABEL.IP_INFO")}
|
||||
<ActionCard icon="Network" classname="tp__ipinfo" title={translate.t("LABEL.IP_INFO")}
|
||||
subtitle_html={if *config_exists {
|
||||
if let Some(ip_info) = &*ip_address {
|
||||
let ip4 = ip_info.ipv4.as_ref().map_or_else(|| "n/a".to_string(), |ip| ip.to_string());
|
||||
|
||||
@@ -14,16 +14,15 @@ pub struct StatusCardProps {
|
||||
|
||||
#[function_component]
|
||||
pub fn StatusCard(props: &StatusCardProps) -> Html {
|
||||
|
||||
html! {
|
||||
<div class={if props.classname.is_empty() {"status-card".to_string()} else {format!("status-card {}", props.classname)}}>
|
||||
<span class="status-card__title">
|
||||
<div class={classes!("tp__status-card", if props.classname.is_empty() { String::new() } else {props.classname.to_string()})}>
|
||||
<span class="tp__status-card__title">
|
||||
{props.title.clone()}
|
||||
</span>
|
||||
<div class="status-card__body">
|
||||
<div class="tp__status-card__body">
|
||||
{props.data.clone()}
|
||||
</div>
|
||||
<div class="status-card__footer">
|
||||
<div class="stp__tatus-card__footer">
|
||||
{props.footer.clone()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,12 +40,12 @@ pub fn DashboardView() -> Html {
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="dashboard">
|
||||
<div class="dashboard__header">
|
||||
<div class="tp__dashboard">
|
||||
<div class="tp__dashboard__header">
|
||||
<h1>{ translate.t("LABEL.DASHBOARD")}</h1>
|
||||
</div>
|
||||
<div class="dashboard__body">
|
||||
<div class="dashboard__body-actions">
|
||||
<div class="tp__dashboard__body">
|
||||
<div class="tp__dashboard__body-actions">
|
||||
<Card>
|
||||
<VersionActionCard version={(*status).as_ref().map_or_else(String::new, |s| s.version.clone())}
|
||||
build_time={(*status).as_ref().map_or_else(String::new, |s| s.build_time.as_ref().map_or_else(String::new, |v| v.clone()))}/>
|
||||
@@ -55,7 +55,7 @@ pub fn DashboardView() -> Html {
|
||||
<Card><DiscordActionCard /></Card>
|
||||
<Card><IpinfoActionCard /></Card>
|
||||
</div>
|
||||
<div class="dashboard__body-stats">
|
||||
<div class="tp__dashboard__body-stats">
|
||||
<Card><StatusCard title={translate.t("LABEL.MEMORY")} data={status.as_ref().map_or_else(|| "n/a".to_string(), |status| status.memory.clone())} /></Card>
|
||||
<Card><StatusCard title={translate.t("LABEL.CACHE")} data={status.as_ref().map_or_else(|| "n/a".to_string(), |status| status.cache.as_ref().map_or_else(|| "n/a".to_string(), |c| c.clone()))} /></Card>
|
||||
<Card><StatusCard title={translate.t("LABEL.ACTIVE_USERS")} data={status.as_ref().map_or_else(|| "n/a".to_string(), |status| status.active_users.to_string())} /></Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::{IconButton, Sidebar, DashboardView};
|
||||
use crate::app::components::{IconButton, Sidebar, DashboardView, PlaylistView};
|
||||
use crate::model::ViewType;
|
||||
use crate::hooks::use_service_context;
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 view_visible = use_state(|| ViewType::Dashboard);
|
||||
let view_visible = use_state(|| ViewType::Playlists);
|
||||
|
||||
let handle_logout = {
|
||||
let services_ctx = services.clone();
|
||||
@@ -24,13 +24,13 @@ pub fn Home() -> Html {
|
||||
// <div class={"app-header__toolbar"}><button data-tooltip={preferencesVisible ? "LABEL.PLAYLIST_BROWSER" : "LABEL.CONFIGURATION"} onClick={handlePreferences}>{getIconByName(preferencesVisible ? "Live" : "Config")}</button></div>
|
||||
|
||||
html! {
|
||||
<div class="app">
|
||||
<div class="tp__app">
|
||||
<Sidebar onview={handle_view_change}/>
|
||||
|
||||
<div class="app-main">
|
||||
<div class="app-main__header app-header">
|
||||
<div class="tp__app-main">
|
||||
<div class="tp__app-main__header tp__app-header">
|
||||
{app_title}
|
||||
<div class={"app-header-toolbar"}>
|
||||
<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>)
|
||||
// }</select>
|
||||
@@ -47,10 +47,11 @@ pub fn Home() -> Html {
|
||||
<IconButton name="Logout" icon="Logout" onclick={handle_logout} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-main__body">
|
||||
<div class="tp__app-main__body">
|
||||
{
|
||||
match *view_visible {
|
||||
ViewType::Dashboard => html! { <DashboardView/> },
|
||||
ViewType::Playlists => html! { <PlaylistView/> },
|
||||
_ => html! { <div>{"Unknown view"}</div> },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn IconButton(props: &IconButtonProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<button class="icon-button" onclick={handle_click}>
|
||||
<button class="tp__icon-button" onclick={handle_click}>
|
||||
<AppIcon name={props.icon.clone()}></AppIcon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ pub struct InputProps {
|
||||
#[function_component]
|
||||
pub fn Input(props: &InputProps) -> Html {
|
||||
html! {
|
||||
<div class="input">
|
||||
<div class="tp__input">
|
||||
{ if props.label.is_some() {
|
||||
html! {
|
||||
<label>{props.label.clone().unwrap_or_default()}</label>
|
||||
}
|
||||
} else { html!{} }
|
||||
}
|
||||
<div class="input-wrapper">
|
||||
<div class="tp__input-wrapper">
|
||||
<input ref={props.input_ref.clone().unwrap_or_default()}
|
||||
type={props.input_type.clone()}
|
||||
name={props.name.clone()}
|
||||
|
||||
@@ -67,18 +67,18 @@ pub fn Login() -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="login-view">
|
||||
<div class={"login-view__header"}>
|
||||
<div class={"login-view__header-logo"}>{app_logo.as_ref().clone()}</div>
|
||||
<div class={"login-view__header-title"}>{ format!("Login to {app_title}") }</div>
|
||||
<div class="tp__login-view">
|
||||
<div class={"tp__login-view__header"}>
|
||||
<div class={"tp__login-view__header-logo"}>{app_logo.as_ref().clone()}</div>
|
||||
<div class={"tp__login-view__header-title"}>{ format!("Login to {app_title}") }</div>
|
||||
</div>
|
||||
<form>
|
||||
<div class="login-view__form">
|
||||
<div class="tp__login-view__form">
|
||||
<Input label={translation.t("LABEL.USERNAME")} input_ref={username_ref} input_type="text" name="username" autocomplete={true} autofocus={true}/>
|
||||
<Input label={translation.t("LABEL.PASSWORD")} input_ref={password_ref} input_type="password" name="password" autocomplete={true} onkeydown={handle_key_down}/>
|
||||
<div class="login-view__form-action">
|
||||
<TextButton style="primary" name="login" title={ translation.t("LABEL.LOGIN")} onclick={handle_login}></TextButton>
|
||||
<span class={if *auth_success { "hidden" } else { "error-text" }}>{ "Failed to login" }</span>
|
||||
<div class="tp__login-view__form-action">
|
||||
<TextButton style="tp__primary" name="login" title={ translation.t("LABEL.LOGIN")} onclick={handle_login}></TextButton>
|
||||
<span class={if *auth_success { "tp__hidden" } else { "error-text" }}>{ "Failed to login" }</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn MenuItem(props: &MenuItemProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="menu-item" onclick={ handle_click }>
|
||||
<div class="tp__menu-item" onclick={ handle_click }>
|
||||
<AppIcon name={props.icon.clone()}></AppIcon>
|
||||
<label>{props.label.clone()}</label>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,11 @@ mod dashboard_view;
|
||||
mod dashboard;
|
||||
mod card;
|
||||
mod text_button;
|
||||
mod playlist_view;
|
||||
mod breadcrumbs;
|
||||
mod playlist;
|
||||
mod table;
|
||||
mod toggle_switch;
|
||||
|
||||
pub use self::svg_icon::*;
|
||||
// pub use self::input::*;
|
||||
@@ -29,4 +34,8 @@ pub use self::sidebar::*;
|
||||
pub use self::preferences::*;
|
||||
pub use self::dashboard_view::*;
|
||||
pub use self::dashboard::*;
|
||||
|
||||
pub use self::playlist_view::*;
|
||||
pub use self::breadcrumbs::*;
|
||||
pub use self::playlist::*;
|
||||
pub use self::table::*;
|
||||
pub use self::toggle_switch::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub struct PanelProps {
|
||||
#[function_component]
|
||||
pub fn Panel(props: &PanelProps) -> Html {
|
||||
html! {
|
||||
<div class={format!("panel{}", if props.value == props.active {""} else {" hidden"} ).to_lowercase()}>
|
||||
<div class={classes!("panel", if props.value == props.active {""} else {"tp__hidden"} )}>
|
||||
{ for props.children.iter() }
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
mod playlist;
|
||||
mod playlist_assistant_context;
|
||||
mod step_name;
|
||||
mod step_type;
|
||||
mod step_schedule;
|
||||
|
||||
pub use self::playlist_assistant_context::*;
|
||||
pub use self::playlist::*;
|
||||
pub use self::step_name::*;
|
||||
pub use self::step_type::*;
|
||||
pub use self::step_schedule::*;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::{NameStep, Panel, PlaylistAssistantContext, TypeStep};
|
||||
|
||||
enum PlaylistAssistantStep {
|
||||
Name,
|
||||
Type,
|
||||
Scheduling,
|
||||
Processing
|
||||
}
|
||||
|
||||
impl fmt::Display for PlaylistAssistantStep {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
PlaylistAssistantStep::Name => "name",
|
||||
PlaylistAssistantStep::Type => "type",
|
||||
PlaylistAssistantStep::Scheduling => "scheduling",
|
||||
PlaylistAssistantStep::Processing => "processing",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PlaylistAssistantStep {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, ()> {
|
||||
match s {
|
||||
"name" => Ok(PlaylistAssistantStep::Name),
|
||||
"type" => Ok(PlaylistAssistantStep::Type),
|
||||
"scheduling" => Ok(PlaylistAssistantStep::Scheduling),
|
||||
"processing" => Ok(PlaylistAssistantStep::Processing),
|
||||
_ => Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[function_component]
|
||||
pub fn PlaylistAssistant() -> Html {
|
||||
let active_step = use_state(|| PlaylistAssistantStep::Name);
|
||||
|
||||
let custom_class = use_state(|| String::new());
|
||||
let context = PlaylistAssistantContext {
|
||||
custom_class: custom_class.clone(),
|
||||
};
|
||||
|
||||
html! {
|
||||
<ContextProvider<PlaylistAssistantContext> context={context}>
|
||||
<div class="tp__playlist-assistant">
|
||||
<Panel value={PlaylistAssistantStep::Name.to_string()} active={active_step.to_string()}>
|
||||
<NameStep/>
|
||||
</Panel>
|
||||
<Panel value={PlaylistAssistantStep::Type.to_string()} active={active_step.to_string()}>
|
||||
<TypeStep/>
|
||||
</Panel>
|
||||
</div>
|
||||
</ContextProvider<PlaylistAssistantContext >>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use yew::UseStateHandle;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct PlaylistAssistantContext {
|
||||
pub custom_class: UseStateHandle<String>,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::PlaylistAssistantContext;
|
||||
|
||||
#[function_component]
|
||||
pub fn NameStep() -> Html {
|
||||
let _playlist_ctx = use_context::<PlaylistAssistantContext>();
|
||||
|
||||
html! {
|
||||
<div class={"tp__playlist-assistant__step-name"}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::PlaylistAssistantContext;
|
||||
|
||||
#[function_component]
|
||||
pub fn ScheduleStep() -> Html {
|
||||
let _playlist_ctx = use_context::<PlaylistAssistantContext>();
|
||||
|
||||
html! {
|
||||
<div class={"tp__playlist-assistant__step-schedule"}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::PlaylistAssistantContext;
|
||||
|
||||
#[function_component]
|
||||
pub fn TypeStep() -> Html {
|
||||
let _playlist_ctx = use_context::<PlaylistAssistantContext>();
|
||||
|
||||
html! {
|
||||
<div class={"tp__playlist-assistant__step-type"}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use yew::UseStateHandle;
|
||||
use crate::app::components::PlaylistPage;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct PlaylistContext {
|
||||
pub active_page: UseStateHandle<PlaylistPage>,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use wasm_logger::Config;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use crate::app::components::{PlaylistContext, PlaylistPage, TextButton};
|
||||
|
||||
#[function_component(PlaylistCreate)]
|
||||
pub fn playlist_create() -> Html {
|
||||
let translate = use_translation();
|
||||
let playlist_ctx = use_context::<PlaylistContext>();
|
||||
|
||||
let handle_back = {
|
||||
let playlist_ctx = playlist_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
if let Some(ctx) = playlist_ctx.as_ref() {
|
||||
ctx.active_page.set(PlaylistPage::Lists);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__playlist-create">
|
||||
<div class="tp__playlist-create__header">
|
||||
<h1>{ translate.t("LABEL.CREATE")}</h1>
|
||||
<TextButton style="primary" name="playlist"
|
||||
icon="Playlist"
|
||||
title={ translate.t("LABEL.LIST")}
|
||||
onclick={handle_back}></TextButton>
|
||||
</div>
|
||||
<div class="tp__playlist-create__body">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use crate::app::components::{PlaylistContext, PlaylistPage, TargetTable, TextButton};
|
||||
|
||||
#[function_component]
|
||||
pub fn PlaylistList() -> Html {
|
||||
let translate = use_translation();
|
||||
let playlist_ctx = use_context::<PlaylistContext>();
|
||||
|
||||
let handle_create = {
|
||||
let playlist_ctx = playlist_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
if let Some(ctx) = playlist_ctx.as_ref() {
|
||||
ctx.active_page.set(PlaylistPage::NewPlaylist);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__playlist-list">
|
||||
<div class="tp__playlist-list__header">
|
||||
<h1>{ translate.t("LABEL.PLAYLISTS")}</h1>
|
||||
<TextButton style="primary" name="new_playlist"
|
||||
icon="PlaylistAdd"
|
||||
title={ translate.t("LABEL.NEW_PLAYLIST")}
|
||||
onclick={handle_create}></TextButton>
|
||||
</div>
|
||||
<div class="tp__playlist-create__body">
|
||||
<TargetTable/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mod assistant;
|
||||
mod list;
|
||||
mod create;
|
||||
mod context;
|
||||
mod page;
|
||||
mod target_table;
|
||||
|
||||
pub use self::assistant::*;
|
||||
pub use self::list::*;
|
||||
pub use self::create::*;
|
||||
pub use self::context::*;
|
||||
pub use self::page::*;
|
||||
pub use self::target_table::*;
|
||||
@@ -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 PlaylistPage {
|
||||
Lists,
|
||||
NewPlaylist,
|
||||
}
|
||||
|
||||
impl FromStr for PlaylistPage {
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, TuliproxError> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"list" => Ok(PlaylistPage::Lists),
|
||||
"create" => Ok(PlaylistPage::NewPlaylist),
|
||||
_ => Err(info_err!(format!("Unknown page type: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PlaylistPage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
Self::Lists => "list",
|
||||
Self::NewPlaylist => "create",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::ConfigTargetDto;
|
||||
use crate::app::components::{PlaylistContext, Table, TableDefinition, ToggleSwitch};
|
||||
|
||||
const HEADERS: [&str; 11] = [
|
||||
"TABLE.ID",
|
||||
"TABLE.ENABLED",
|
||||
"TABLE.NAME",
|
||||
"TABLE.OPTIONS",
|
||||
"TABLE.SORT",
|
||||
"TABLE.FILTER",
|
||||
"TABLE.OUTPUT",
|
||||
"TABLE.RENAME",
|
||||
"TABLE.MAPPING",
|
||||
"TABLE.PROCESSING_ORDER",
|
||||
"TABLE.WATCH",
|
||||
];
|
||||
|
||||
#[function_component]
|
||||
pub fn TargetTable() -> Html {
|
||||
let translate = use_translation();
|
||||
let playlist_ctx = use_context::<PlaylistContext>();
|
||||
|
||||
let render_header_cell = {
|
||||
Callback::<usize, Html>::from(move |col| {
|
||||
html! {
|
||||
{
|
||||
if col < HEADERS.len() {
|
||||
translate.t(HEADERS[col])
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let render_data_cell = {
|
||||
Callback::<(usize, usize, &ConfigTargetDto), Html>::from(
|
||||
move |(_row, col, dto): (usize, usize, &ConfigTargetDto)| {
|
||||
match col {
|
||||
0 => html! { &dto.name.to_string() },
|
||||
1 => html! { <ToggleSwitch value={&dto.enabled} /> },
|
||||
2 => html! { &dto.name.to_string() },
|
||||
3 => html! { "options" },
|
||||
4 => html! { "sort" },
|
||||
5 => html! { "filter" },
|
||||
6 => html! { "output" },
|
||||
7 => html! { "rename" },
|
||||
8 => html! { "mapping" },
|
||||
9 => html! { "processing order" },
|
||||
10 => html! { "watch" },
|
||||
_ => html! {""},
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let items: Vec<&ConfigTargetDto> = vec![];
|
||||
|
||||
let table_definition = {
|
||||
let render_header_cell = render_header_cell.clone();
|
||||
let render_data_cell = render_data_cell.clone();
|
||||
|
||||
use_state(|| Rc::new(TableDefinition::<&ConfigTargetDto> {
|
||||
items,
|
||||
num_cols: 11,
|
||||
render_header_cell,
|
||||
render_data_cell,
|
||||
}))
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__target-table">
|
||||
<Table::<&ConfigTargetDto> definition={(*table_definition).clone()} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use crate::app::components::{Breadcrumbs, Panel, PlaylistContext, PlaylistCreate, PlaylistList, PlaylistPage};
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
#[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::Lists);
|
||||
|
||||
let handle_breadcrumb_select = {
|
||||
let view_visible = active_page.clone();
|
||||
Callback::from(move |(_name, index)| {
|
||||
if index == 0 && *view_visible != PlaylistPage::Lists {
|
||||
view_visible.set(PlaylistPage::Lists);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
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::Lists => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")])),
|
||||
PlaylistPage::NewPlaylist => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.CREATE")])),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let context = PlaylistContext {
|
||||
active_page: active_page.clone(),
|
||||
};
|
||||
|
||||
html! {
|
||||
<ContextProvider<PlaylistContext> context={context}>
|
||||
<div class="tp__playlist-view">
|
||||
<Breadcrumbs items={&*breadcrumbs} onclick={ handle_breadcrumb_select }/>
|
||||
<div class="tp__playlist-view__body">
|
||||
<Panel value={PlaylistPage::Lists.to_string()} active={active_page.to_string()}>
|
||||
<PlaylistList />
|
||||
</Panel>
|
||||
<Panel value={PlaylistPage::NewPlaylist.to_string()} active={active_page.to_string()}>
|
||||
<PlaylistCreate />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</ContextProvider<PlaylistContext>>
|
||||
}
|
||||
}
|
||||
@@ -79,15 +79,15 @@ pub fn Preferences() -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="preferences">
|
||||
<div class="preferences__content">
|
||||
<div class="preferences__sidebar">
|
||||
<div class="tp__preferences">
|
||||
<div class="tp__preferences__content">
|
||||
<div class="tp__preferences__sidebar">
|
||||
{
|
||||
SIDEBAR_ACTIONS.iter().map(|action| {
|
||||
html! {
|
||||
<div key={format!("pref_{:?}", action.page).to_lowercase()}
|
||||
data-page={action.page.to_string()}
|
||||
class={format!("preferences__sidebar-menu-action preferences__sidebar-menu-action_{}{}",
|
||||
class={format!("tp__preferences__sidebar-menu-action preferences__sidebar-menu-action_{}{}",
|
||||
action.page.to_string(),
|
||||
if action.page == *active_page { " selected" } else { "" })}
|
||||
onclick={handle_sidebar_action.clone()}
|
||||
@@ -99,21 +99,21 @@ pub fn Preferences() -> Html {
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</div>
|
||||
<div class="preferences__panels">
|
||||
<div class="tp__preferences__panels">
|
||||
<Panel value={PrefPage::Update.to_string()} active={active_page.to_string()}>
|
||||
<div class="card">{"Update"}</div>
|
||||
<div class="tp__card">{"Update"}</div>
|
||||
</Panel>
|
||||
<Panel value={PrefPage::User.to_string()} active={active_page.to_string()}>
|
||||
<div class="card">{"User"}</div>
|
||||
<div class="tp__card">{"User"}</div>
|
||||
</Panel>
|
||||
<Panel value={PrefPage::ApiServer.to_string()} active={active_page.to_string()}>
|
||||
<div class="card">{"Api"}</div>
|
||||
<div class="tp__card">{"Api"}</div>
|
||||
</Panel>
|
||||
<Panel value={PrefPage::MainConfig.to_string()} active={active_page.to_string()}>
|
||||
<div class="card">{"MainConfig"}</div>
|
||||
<div class="tp__card">{"MainConfig"}</div>
|
||||
</Panel>
|
||||
<Panel value={PrefPage::Status.to_string()} active={active_page.to_string()}>
|
||||
<div class="card">{"Status"}</div>
|
||||
<div class="tp__card">{"Status"}</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,16 +34,20 @@ pub fn Sidebar(props: &SidebarProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="app-sidebar">
|
||||
<div class="app-sidebar__header app-header">
|
||||
<span class="app-header__logo">{app_logo}</span>
|
||||
<div class="tp__app-sidebar">
|
||||
<div class="tp__app-sidebar__header tp__app-header">
|
||||
<span class="tp__app-header__logo">{app_logo}</span>
|
||||
<AppIcon name={"ChevronLeft"}></AppIcon>
|
||||
</div>
|
||||
<div class="app-sidebar__content">
|
||||
<MenuItem icon="Dashboard" name="dashboard" label={translate.t("LABEL.DASHBOARD")}
|
||||
<div class="tp__app-sidebar__content">
|
||||
<MenuItem icon="DashboardOutline" name="dashboard" label={translate.t("LABEL.DASHBOARD")}
|
||||
onclick={&handle_menu_click}></MenuItem>
|
||||
<CollapsePanel title={translate.t("LABEL.SETTINGS")}>
|
||||
<MenuItem icon="User" name="users" label={translate.t("LABEL.USER")}
|
||||
<MenuItem icon="UserOutline" name="users" label={translate.t("LABEL.USER")}
|
||||
onclick={&handle_menu_click}></MenuItem>
|
||||
</CollapsePanel>
|
||||
<CollapsePanel title={translate.t("LABEL.PLAYLIST")}>
|
||||
<MenuItem icon="PlayArrowOutline" name="playlists" label={translate.t("LABEL.PLAYLIST")}
|
||||
onclick={&handle_menu_click}></MenuItem>
|
||||
</CollapsePanel>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn SvgIcon(props: &SvgIconProps) -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq, Eq)]
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct AppIconProps {
|
||||
pub name: AttrValue,
|
||||
#[prop_or(AttrValue::Static("100%"))]
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TableDefinition<T: PartialEq + Clone + 'static> {
|
||||
pub items: Vec<T>,
|
||||
pub num_cols: usize,
|
||||
pub render_header_cell: Callback<usize, Html>,
|
||||
pub render_data_cell: Callback<(usize, usize, T), Html>,
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct TableProps<T: PartialEq + Clone + 'static> {
|
||||
pub definition: Rc<TableDefinition<T>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Table<T: PartialEq + Clone + 'static>(props: &TableProps<T>) -> Html {
|
||||
let TableDefinition {
|
||||
items,
|
||||
num_cols,
|
||||
render_header_cell,
|
||||
render_data_cell,
|
||||
} = &*props.definition;
|
||||
|
||||
html! {
|
||||
<div class={"tp__table"}>
|
||||
<div class={"tp__table__container"}>
|
||||
<table class="tp__table__table">
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
for (0..*num_cols).map(|col_index| {
|
||||
html!{
|
||||
<th> {render_header_cell.emit(col_index)}</th>
|
||||
}
|
||||
})
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
for items.iter().enumerate().map(|(row_index, item)| {
|
||||
let item_clone = item.clone();
|
||||
html! {
|
||||
<tr>
|
||||
{
|
||||
for (0..*num_cols).map(|col_index| {
|
||||
html!{
|
||||
<td>{render_data_cell.emit((row_index, col_index, item_clone.clone()))}</td>
|
||||
}
|
||||
})
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub fn TextButton(props: &TextButtonProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<button class={if props.style.is_empty() {"text-button".to_string()} else {format!("text-button button-{}", props.style)}} onclick={handle_click}>
|
||||
<button class={if props.style.is_empty() {"tp__text-button".to_string()} else {format!("tp__text-button tp__button-{}", props.style)}} onclick={handle_click}>
|
||||
if !props.icon.is_empty() {
|
||||
<AppIcon name={props.icon.clone()}></AppIcon>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, Clone, PartialEq, Debug)]
|
||||
pub struct ToggleSwitchProps {
|
||||
#[prop_or_default]
|
||||
pub value: bool,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ToggleSwitch(props: &ToggleSwitchProps) -> Html {
|
||||
let toggled = use_state(|| props.value);
|
||||
|
||||
let onclick = {
|
||||
let toggled = toggled.clone();
|
||||
Callback::from(move |_| toggled.set(!*toggled))
|
||||
};
|
||||
|
||||
html! {
|
||||
<label class="tp__toggle-switch">
|
||||
<input type="checkbox"
|
||||
checked={*toggled}
|
||||
onclick={onclick}/>
|
||||
<span class={classes!("tp__toggle-switch__track", if *toggled { "tp__toggle-switch__active" } else { "" })}>
|
||||
<span class={classes!("tp__toggle-switch__toggle", if *toggled { "tp__toggle-switch__on" } else { "" })}>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
mod components;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use futures::future::join_all;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
use shared::error::{info_err, TuliproxError, TuliproxErrorKind};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ViewType {
|
||||
Dashboard,
|
||||
Users,
|
||||
Playlists,
|
||||
}
|
||||
|
||||
impl FromStr for ViewType {
|
||||
@@ -13,6 +15,7 @@ impl FromStr for ViewType {
|
||||
match s.to_lowercase().as_str() {
|
||||
"dashboard" => Ok(ViewType::Dashboard),
|
||||
"users" => Ok(ViewType::Users),
|
||||
"playlists" => Ok(ViewType::Playlists),
|
||||
_ => Err(info_err!(format!("Unknown view type: {s}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ pub struct IconContextProps {
|
||||
pub icons: Vec<Rc<IconDefinition>>,
|
||||
}
|
||||
|
||||
/// User context provider.
|
||||
#[function_component]
|
||||
pub fn IconContextProvider(props: &IconContextProps) -> Html {
|
||||
let icon_ctx = use_state(||IconContext::new(&props.icons));
|
||||
|
||||
@@ -9,7 +9,6 @@ pub struct ServiceContextProps {
|
||||
pub config: WebConfig
|
||||
}
|
||||
|
||||
/// User context provider.
|
||||
#[function_component]
|
||||
pub fn ServiceContextProvider(props: &ServiceContextProps) -> Html {
|
||||
let service_ctx = use_state(||ServiceContext::new(&props.config));
|
||||
|
||||
Reference in New Issue
Block a user