diff --git a/shared/src/model/cluster_flags.rs b/shared/src/model/cluster_flags.rs index d2fa8f863..50fa95e17 100644 --- a/shared/src/model/cluster_flags.rs +++ b/shared/src/model/cluster_flags.rs @@ -67,7 +67,7 @@ impl TryFrom<&str> for ClusterFlags { type Error = &'static str; fn try_from(value: &str) -> Result { - let input = value.trim().trim_matches(['[', ']'].as_ref()); + let input = value.trim().trim_matches(|c| ['[', ']', '(', ')'].contains(&c)); let items = input.split(',').map(str::trim); ClusterFlags::from_items(items) } diff --git a/shared/src/model/config/api_user.rs b/shared/src/model/config/api_user.rs index f79a5d7b5..d6c78b468 100644 --- a/shared/src/model/config/api_user.rs +++ b/shared/src/model/config/api_user.rs @@ -247,5 +247,6 @@ impl ProxyUserCredentialsDto { } true } +} + -} \ No newline at end of file diff --git a/shared/src/model/status_check.rs b/shared/src/model/status_check.rs index d75c61efe..8ead50762 100644 --- a/shared/src/model/status_check.rs +++ b/shared/src/model/status_check.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct StatusCheck { pub status: String, pub version: String, diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 1b64c3e21..4a366252f 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -83,6 +83,17 @@ pub fn trim_last_slash(s: &str) -> Cow { Cow::Borrowed(s) } +pub trait Substring { + fn substring(&self, from: usize, to: usize) -> String; +} + +impl Substring for String { + fn substring(&self, from: usize, to: usize) -> String { + self.chars().skip(from).take(to - from).collect() + } +} + + #[cfg(test)] mod test { use std::collections::HashSet; diff --git a/shared/src/utils/time_utils.rs b/shared/src/utils/time_utils.rs index 7140c1e5d..0fb480747 100644 --- a/shared/src/utils/time_utils.rs +++ b/shared/src/utils/time_utils.rs @@ -1,8 +1,22 @@ use std::time::{SystemTime, UNIX_EPOCH}; +use chrono::{DateTime}; pub fn current_time_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() +} + +pub fn unix_ts_to_str(ts: i64) -> Option { + if ts > 0 { + let normalized_ts = if ts > 4102444800 { + ts / 1000 + } else { + ts + }; + DateTime::from_timestamp(normalized_ts, 0).map(|dt| dt.format("%d.%m.%Y").to_string()) + } else { + None + } } \ No newline at end of file diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json index 43f8249df..c8fd37583 100644 --- a/webui/public/assets/i18n/en.json +++ b/webui/public/assets/i18n/en.json @@ -7,6 +7,7 @@ "SUBMIT": "Submit", "OK": "Ok", "CANCEL": "Cancel", + "YES": "Yes", "CLOSE": "Close", "SHOW_SELECTED": "Show selected", "SELECT_ALL": "Select All", @@ -56,7 +57,8 @@ "EPISODE_PATTERN": "Episode Pattern", "API": "Api", "MAIN": "Main", - "REVERSE_PROXY": "ReverseProxy", + "REVERSE": "Reverse", + "REDIRECT": "Redirect", "MESSAGING": "Messaging", "VIDEO": "Video", "EXAMPLES": "Examples", @@ -126,12 +128,16 @@ "TARGETS": "Targets", "NOTES": "Notes", "DASHBOARD": "Dashboard", + "STATS": "Stats", "SETTINGS": "Settings", "WELCOME": "Welcome", "RELEASES": "Releases", "DISCORD": "Discord", "OPEN_DISCORD": "Open Discord", "JOIN_ON_DISCORD": "Join us on Discord", + "GITHUB": "GitHub", + "OPEN_GITHUB": "Open GitHub", + "STAR_ON_GITHUB": "If you like this project, star ⭐ us on GitHub", "DOCUMENTATION": "Documentation", "DOCUMENTATION_CONTENT": "Get started with tuliprox: features, setup, and examples", "OPEN_DOCUMENTATION": "Documentation", diff --git a/webui/public/assets/icons.json b/webui/public/assets/icons.json index 3fbd22043..9e1bbfa6c 100644 --- a/webui/public/assets/icons.json +++ b/webui/public/assets/icons.json @@ -191,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": [ + "Userlist" + ], + "path": "M 2,2 V 4.133168 H 16.250575 V 2 Z M 2,6.2668527 V 8.4000206 H 16.250575 V 6.2668527 Z m 0,4.2663363 v 2.133168 h 9.068547 v -2.133168 z m 14.999871,1.466553 c -1.38123,0 -2.500065,1.118836 -2.500065,2.500064 0,1.381228 1.118835,2.500065 2.500065,2.500065 1.381227,0 2.500064,-1.118837 2.500064,-2.500065 0,-1.381228 -1.118837,-2.500064 -2.500064,-2.500064 z m 0,6.250161 c -1.668724,0 -5.000129,0.837591 -5.000129,2.500065 V 22 H 22 v -1.250032 c 0,-1.662474 -3.331407,-2.500065 -5.000129,-2.500065 z" + }, { "keys": [ "UserOutline" @@ -327,6 +333,10 @@ "keys": ["Discord"], "path": "M 20.330282,3.5559333 C 18.776719,2.8290591 17.115578,2.300797 15.378897,2 15.165611,2.3856111 14.916435,2.9042666 14.744634,3.3168551 12.8985,3.0392189 11.069342,3.0392189 9.2571568,3.3168551 9.0853913,2.9042666 8.8305533,2.3856111 8.6153611,2 6.876807,2.300797 5.2137558,2.8310001 3.6601933,3.5597827 0.52664291,8.2949932 -0.32281111,12.91259 0.10191661,17.464618 2.1802519,19.01667 4.194409,19.959493 6.1745754,20.57645 6.6634889,19.903565 7.0995344,19.188273 7.4751767,18.43442 6.7597522,18.162573 6.0745289,17.827099 5.4270738,17.437641 5.5988404,17.310387 5.766856,17.177343 5.9291803,17.040449 c 3.9490153,1.847057 8.2397197,1.847057 12.1415557,0 0.164233,0.136894 0.332217,0.269938 0.502108,0.397192 -0.649364,0.391368 -1.336461,0.726842 -2.051886,0.99872 0.37564,0.751912 0.809812,1.469146 1.300599,2.141999 1.982076,-0.616957 3.998101,-1.559751 6.076439,-3.113742 C 24.396351,12.187657 23.046663,7.6124671 20.330282,3.5559333 Z M 8.013176,14.665162 c -1.1854543,0 -2.1576239,-1.106695 -2.1576239,-2.454377 0,-1.347683 0.9514103,-2.4562854 2.1576239,-2.4562854 1.2062483,0 2.178383,1.1066624 2.157622,2.4562854 0.0018,1.347682 -0.9513737,2.454377 -2.157622,2.454377 z m 7.973564,0 c -1.185452,0 -2.157622,-1.106695 -2.157622,-2.454377 0,-1.347683 0.951378,-2.4562854 2.157622,-2.4562854 1.206216,0 2.178383,1.1066624 2.157626,2.4562854 0,1.347682 -0.95141,2.454377 -2.157626,2.454377 z" }, + { + "keys": ["Github"], + "path": "M 10.899231,21.949392 C 5.6132891,21.280424 1.7576546,16.774294 2.0118743,11.562623 2.1366943,9.0037628 3.1299406,6.7326232 4.9060902,4.9447597 6.3931372,3.4479048 8.1987817,2.503358 10.327657,2.1086945 c 0.700524,-0.1298668 2.53425,-0.1474176 3.202971,-0.030656 2.207714,0.3854764 4.039774,1.320844 5.555132,2.8362014 1.515358,1.5153577 2.450724,3.3474172 2.836201,5.5551321 0.116763,0.668721 0.09921,2.502448 -0.03065,3.202969 -0.394663,2.128877 -1.339212,3.934522 -2.836065,5.42157 -1.466656,1.457045 -3.266741,2.388523 -5.349004,2.76791 -0.60779,0.110738 -2.225226,0.161199 -2.807006,0.08757 z M 9.8257213,19.13151 v -0.684065 l -0.8654925,0.0044 c -0.7845655,0.004 -0.8950981,-0.009 -1.1821532,-0.138821 C 7.3611272,18.124455 6.9754809,17.70228 6.6758714,17.106384 6.3897054,16.537231 6.2030917,16.301752 5.8405086,16.052289 5.6940324,15.951512 5.5741868,15.831324 5.5741868,15.785208 c 0,-0.28063 0.5506338,-0.368114 0.9700836,-0.154126 0.1300263,0.06633 0.35442,0.262291 0.4986558,0.435458 0.8434254,1.01262 1.2208193,1.274697 1.8355836,1.274697 0.1868089,0 0.4863793,-0.04265 0.6657138,-0.09479 0.3129282,-0.09097 0.327358,-0.104437 0.3581871,-0.334288 0.025076,-0.186954 0.2278963,-0.693604 0.3919983,-0.979219 0.01045,-0.01818 -0.195895,-0.06438 -0.4585346,-0.102655 C 8.003399,15.5632 6.7787048,14.440728 6.449378,12.726451 6.2080362,11.47018 6.4278917,10.207331 7.0379495,9.3457509 7.199101,9.1181565 7.2057076,9.0855371 7.1425366,8.8294933 7.1054476,8.6791712 7.0722058,8.2555362 7.0686638,7.8880822 7.062902,7.2895653 7.0780296,7.1883561 7.2140634,6.916303 7.3571973,6.6300364 7.3780936,6.6136742 7.5784805,6.6309718 8.0188159,6.6689811 8.7141514,6.9545843 9.4622981,7.4047345 L 9.7973438,7.6063264 10.221502,7.4886106 C 11.214397,7.2130545 12.816219,7.2151 13.81915,7.4932016 l 0.407952,0.1131218 0.335046,-0.2015919 c 0.748149,-0.4501504 1.443485,-0.7357536 1.883818,-0.7737629 0.200387,-0.017297 0.221283,-9.412e-4 0.364417,0.2853312 0.136027,0.2720533 0.15117,0.3732623 0.145402,0.9717792 -0.0035,0.3674542 -0.03679,0.791089 -0.07387,0.9414111 -0.06317,0.2560438 -0.05656,0.2886634 0.104587,0.5162576 0.610056,0.8615803 0.829914,2.1244293 0.588574,3.3807003 -0.32988,1.717147 -1.550955,2.836302 -3.386497,3.103835 -0.262642,0.03828 -0.468981,0.08448 -0.458537,0.102655 0.166312,0.289462 0.359271,0.778501 0.407489,1.032752 0.03366,0.177491 0.0612,0.888382 0.0612,1.579758 0,0.691377 0.01584,1.272898 0.03522,1.29227 0.05294,0.05294 0.833116,-0.226596 1.387905,-0.497277 0.814818,-0.397552 1.423912,-0.84238 2.16442,-1.580697 1.226241,-1.222618 1.947252,-2.563113 2.287862,-4.253567 0.144422,-0.716774 0.143249,-2.325233 -0.0023,-3.049553 C 19.747144,8.839961 19.03229,7.4798978 17.88662,6.2990877 16.686908,5.0625802 15.316153,4.3080041 13.588102,3.9328301 12.893809,3.7820932 11.101321,3.7810514 10.433014,3.9309936 9.2335887,4.2001029 8.1218948,4.6871943 7.2121214,5.3422354 6.1845506,6.0820934 5.1362526,7.341252 4.6066279,8.4718143 4.04195,9.6772004 3.8480824,10.566162 3.8464755,11.957401 c -0.00184,1.508816 0.2059399,2.414763 0.8497152,3.706577 0.925383,1.856899 2.6814438,3.399091 4.626667,4.063187 0.2264299,0.0773 0.4321884,0.128817 0.457242,0.114477 0.025054,-0.01434 0.04555,-0.333902 0.04555,-0.710138 z" + }, { "keys": ["Book"], "path": "M 7.000129,3.5 C 5.2274325,3.5 3.3181595,3.8649277 2,4.8678527 V 18.224958 c 0,0.227952 0.2274766,0.455778 0.4547451,0.455778 0.090907,0 0.1364628,-0.04547 0.2273726,-0.04547 1.2272517,-0.592636 2.9998519,-1.003023 4.3180113,-1.003023 1.772698,0 3.681452,0.36493 4.999613,1.367852 1.227249,-0.774988 3.454699,-1.367852 5.000129,-1.367852 1.499972,0 3.045305,0.273752 4.318011,0.957548 0.09092,0.04559 0.136459,0.04547 0.227373,0.04547 C 21.772538,18.635262 22,18.407406 22,18.179483 V 4.8678527 C 21.45454,4.457565 20.863342,4.1837157 20.181536,3.9557786 19.172461,3.6366664 18.063487,3.5 16.999871,3.5 15.227172,3.5 13.317898,3.8649277 11.999742,4.8678527 10.681581,3.8649277 8.772827,3.5 7.000129,3.5 Z m 9.999742,1.8236312 c 1.09089,0 2.181683,0.1366646 3.181665,0.4557786 V 16.264386 c -0.999982,-0.319098 -2.090775,-0.455779 -3.181665,-0.455779 -1.545429,0 -3.77288,0.592868 -5.000129,1.367853 V 6.6914839 c 1.227249,-0.7749878 3.4547,-1.3678527 5.000129,-1.3678527 z m 0,2.2794098 c -1.545429,0 -2.945204,0.2641831 -4.090639,0.7565305 v 1.5135777 c 1.027255,-0.5835186 2.454303,-0.9027723 4.090639,-0.9027723 0.799985,0 1.572705,0.08219 2.272692,0.2371908 V 7.8216287 C 18.554394,7.6848681 17.781677,7.603041 16.999871,7.603041 Z m 0,2.425135 c -1.545429,0 -2.945204,0.273802 -4.090639,0.757048 v 1.513061 c 1.027255,-0.583521 2.454303,-0.902256 4.090639,-0.902256 0.799985,0 1.572705,0.08167 2.272692,0.236675 V 10.247281 C 18.554394,10.11051 17.781677,10.028176 16.999871,10.028176 Z m 0,2.434438 c -1.545429,0 -2.945204,0.264188 -4.090639,0.75653 v 1.513578 c 1.027255,-0.583518 2.454303,-0.902256 4.090639,-0.902256 0.799985,0 1.572705,0.08167 2.272692,0.236674 v -1.385939 c -0.718169,-0.145868 -1.490886,-0.218587 -2.272692,-0.218587 z" @@ -337,7 +347,7 @@ }, { "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" + "path": "M 1.9998779,1.9998779 V 18.722923 L 15.141711,10.3614 Z M 4.3893962,6.3479328 10.685136,10.3614 4.3893962,14.374868 Z M 15.684314,11.092652 v 4.362785 h -4.210596 v 2.181656 h 4.210596 v 4.362785 h 2.105298 v -4.362785 h 4.210595 v -2.181656 h -4.210595 v -4.362785 z" }, { "keys": ["Playlist"], @@ -370,5 +380,9 @@ { "keys": ["Target", "PlayCircle"], "path": "M 11.999998,2 C 6.4799985,2 2,6.4799985 2,11.999998 2,17.519998 6.4799985,22 11.999998,22 17.519998,22 22,17.519998 22,11.999998 22,6.4799985 17.519998,2 11.999998,2 Z m 0,17.999999 c -4.4099977,0 -7.9999969,-3.589999 -7.9999969,-8.000001 0,-4.4099977 3.5899992,-7.9999969 7.9999969,-7.9999969 4.410002,0 8.000001,3.5899992 8.000001,7.9999969 0,4.410002 -3.589999,8.000001 -8.000001,8.000001 z M 10.000001,16.499998 16,11.999998 10.000001,7.4999984 Z" + }, + { + "keys": ["Stats"], + "path": "m 14.852111,10.994975 h 7.14787 C 21.528804,6.2512562 17.759381,2.4723619 13.027548,2 v 7.1658292 c 0.852132,0.3115584 1.513786,0.9748738 1.824563,1.8291458 m 0.18047,-6.4522612 c 2.005014,0.8241205 3.609023,2.4321605 4.431077,4.4422109 H 16.025046 C 15.744341,8.6130655 15.40349,8.2713567 15.032562,7.9899498 Z M 2,12 c 0,5.21608 3.9598995,9.497487 9.022557,10 V 14.834171 C 9.8596486,14.41206 9.0175433,13.306533 9.0175433,12 c 0,-1.306533 0.8421053,-2.4120603 2.0050137,-2.8341708 V 2 C 5.9598995,2.5025127 2,6.7839197 2,12 M 9.0175433,4.5427138 V 7.9999999 C 7.7844623,8.9246232 7.0125293,10.40201 7.0125293,12 c 0,1.59799 0.771933,3.075377 2.005014,4.01005 v 3.457287 C 6.0501232,18.271357 4.0050102,15.366834 4.0050102,12 c 0,-3.3668343 2.045113,-6.2713567 5.0125331,-7.4572862 M 13.027567,14.834171 V 22 C 17.7594,21.527638 21.528823,17.748744 22,13.005025 h -7.14787 c -0.310774,0.854272 -0.972432,1.517588 -1.824563,1.829146 m 2.005014,1.175879 c 0.370944,-0.281406 0.711779,-0.613065 0.992484,-0.994975 h 3.438593 c -0.822054,2.010051 -2.426063,3.618091 -4.431077,4.442211 z" } ] \ No newline at end of file diff --git a/webui/scss/_default.scss b/webui/scss/_default.scss index fb2e961e6..f12654741 100644 --- a/webui/scss/_default.scss +++ b/webui/scss/_default.scss @@ -89,4 +89,5 @@ li { .tp__full-width { box-sizing: border-box; width: 100%; + align-items: center; } \ No newline at end of file diff --git a/webui/scss/_theme.scss b/webui/scss/_theme.scss index 4265500ac..12b4a5fef 100644 --- a/webui/scss/_theme.scss +++ b/webui/scss/_theme.scss @@ -68,6 +68,7 @@ --discord-color: #5764f0; --documentation-color: #2b944b; + --github-color: #ffffff; --app-header-background-color: #18181b; --app-header-color: #73d6fe; @@ -81,17 +82,37 @@ --tag-color: #e0e0e0; --tag-background-color: #2c2f36; - --tag-active-border-color: #2b6140; --tag-active-color: #49dc7f; --tag-active-background-color: #1e2d25; + --tag-active-border-color: #2b6140; - --tag-inactive-border-color: #555; --tag-inactive-color: #6f6f6f; --tag-inactive-background-color: #3a3a3a; + --tag-inactive-border-color: #555; - --tag-alt-active-border-color: #2b4c61; --tag-alt-active-color: #49dcdc; --tag-alt-active-background-color: #1e252d; + --tag-alt-active-border-color: #2b4c61; + + //--tag-status-expired-color: #b7af91; + //--tag-status-expired-background-color: #4b421f; + //--tag-status-expired-border-color: #645828; + + --tag-status-expired-color: #f3e1ae; + --tag-status-expired-background-color: #6b5e33; + --tag-status-expired-border-color: #a99653; + + --tag-status-banned-color: #dd7573; + --tag-status-banned-background-color: #4c3434; + --tag-status-banned-border-color: #755252; + + --tag-status-trial-color: #49dcdc; + --tag-status-trial-background-color: #1e252d; + --tag-status-trial-border-color: #2b4c61; + + --tag-status-pending-color: #49dcdc; + --tag-status-pending-background-color: #1e252d; + --tag-status-pending-border-color: #2b4c61; --popup-background-color: #313133; //#18181b; --popup-shadow: 0 3px 8px #06070a; diff --git a/webui/scss/app/_component.scss b/webui/scss/app/_component.scss index db5538a7d..416d9747f 100644 --- a/webui/scss/app/_component.scss +++ b/webui/scss/app/_component.scss @@ -8,14 +8,15 @@ @forward "components/table"; @forward "components/panel"; @forward "components/card"; -@forward "components/dashboard/action_card"; -@forward "components/dashboard/status_card"; @forward "components/collapse_panel"; @forward "components/breadcrumbs"; @forward "components/login"; @forward "components/sidebar"; @forward "components/preferences"; +@forward "components/dashboard/action_card"; +@forward "components/dashboard/status_card"; @forward "components/dashboard/dashboard_view"; +@forward "components/dashboard/stats_view"; @forward "components/list_view"; @forward "components/userlist/userlist_view"; @forward "components/playlist/playlist_view"; @@ -42,3 +43,6 @@ @forward "components/popup_menu"; @forward "components/dialog"; @forward "components/filter"; +@forward "components/userlist/user_state"; +@forward "components/userlist/proxy_type"; +@forward "components/userlist/max_connections"; diff --git a/webui/scss/app/components/_chip.scss b/webui/scss/app/components/_chip.scss index be1049b8e..8d40ed9c7 100644 --- a/webui/scss/app/components/_chip.scss +++ b/webui/scss/app/components/_chip.scss @@ -2,6 +2,7 @@ display: inline-flex; flex-flow: row; align-items: center; + justify-content: center; padding: var(--padding-micro) var(--padding-mini); cursor: default; color: var(--tag-color); @@ -9,6 +10,10 @@ border: 1px solid var(--tag-border-color); border-radius: var(--tag-border-radius); + &__label { + + } + &__remove { margin-left: 0.5rem; font-weight: bold; diff --git a/webui/scss/app/components/_dialog.scss b/webui/scss/app/components/_dialog.scss index e7818431b..18a578e7c 100644 --- a/webui/scss/app/components/_dialog.scss +++ b/webui/scss/app/components/_dialog.scss @@ -11,6 +11,7 @@ dialog { &::backdrop { background: rgba(0, 0, 0, 0.3); } + max-width: 80vw; } .tp__dialog { diff --git a/webui/scss/app/components/_sidebar.scss b/webui/scss/app/components/_sidebar.scss index 069b46e27..d984ec7d4 100644 --- a/webui/scss/app/components/_sidebar.scss +++ b/webui/scss/app/components/_sidebar.scss @@ -12,6 +12,10 @@ $sidebar-width: 270px; display: flex; flex-flow: column; padding: 2rem 1.5rem; - gap: var(--gap-larger); + gap: var(--gap-default); + + .tp__collapse-panel { + margin-top: var(--gap-large); + } } } diff --git a/webui/scss/app/components/dashboard/_action_card.scss b/webui/scss/app/components/dashboard/_action_card.scss index 2113b168c..7a37037e5 100644 --- a/webui/scss/app/components/dashboard/_action_card.scss +++ b/webui/scss/app/components/dashboard/_action_card.scss @@ -14,6 +14,12 @@ img { width: 3rem; } + .svg-icon { + width: 3rem; + height: 3rem; + max-width: 3rem; + max-height: 3rem; + } } &__body { @@ -33,4 +39,11 @@ &__content { } + + .tp__text-button { + span { + white-space: nowrap; + } + } + } \ No newline at end of file diff --git a/webui/scss/app/components/dashboard/_dashboard_view.scss b/webui/scss/app/components/dashboard/_dashboard_view.scss index b592fbc00..234dda564 100644 --- a/webui/scss/app/components/dashboard/_dashboard_view.scss +++ b/webui/scss/app/components/dashboard/_dashboard_view.scss @@ -45,9 +45,9 @@ $dashboard-mobile-size: 1280px; .tp__documentation { fill: var(--documentation-color); + } - svg { - transform: scale(1.4); - } + .tp__github { + fill: var(--github-color); } } \ No newline at end of file diff --git a/webui/scss/app/components/dashboard/_stats_view.scss b/webui/scss/app/components/dashboard/_stats_view.scss new file mode 100644 index 000000000..c3ee32039 --- /dev/null +++ b/webui/scss/app/components/dashboard/_stats_view.scss @@ -0,0 +1,25 @@ +.tp__stats { + display: flex; + flex-flow: column; + flex: 1 1 auto; + box-sizing: border-box; + width: 100%; + max-width: var(--max-view-width); + + &__header { + display: flex; + flex-flow: row nowrap; + } + + &__body { + display: flex; + flex-flow: column; + gap: var(--gap-larger); + + &-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--gap-larger); + } + } +} \ No newline at end of file diff --git a/webui/scss/app/components/userlist/_max_connections.scss b/webui/scss/app/components/userlist/_max_connections.scss new file mode 100644 index 000000000..1b682c365 --- /dev/null +++ b/webui/scss/app/components/userlist/_max_connections.scss @@ -0,0 +1,7 @@ +.tp__max-connections { + display: flex; + flex-flow: row; + justify-content: flex-end; + align-items: flex-end; + text-align: right; +} \ No newline at end of file diff --git a/webui/scss/app/components/userlist/_proxy_type.scss b/webui/scss/app/components/userlist/_proxy_type.scss new file mode 100644 index 000000000..1a7c91340 --- /dev/null +++ b/webui/scss/app/components/userlist/_proxy_type.scss @@ -0,0 +1,17 @@ +.tp__proxy-type { + display: flex; + flex-flow: row nowrap; + gap: var(--gap-small); + + &__reverse { + color: var(--tag-active-color); + background-color: var(--tag-active-background-color); + border-color: var(--tag-active-border-color); + } + + &__redirect { + color: var(--tag-alt-active-color); + background-color: var(--tag-alt-active-background-color); + border-color: var(--tag-alt-active-border-color); + } +} \ No newline at end of file diff --git a/webui/scss/app/components/userlist/_user_state.scss b/webui/scss/app/components/userlist/_user_state.scss new file mode 100644 index 000000000..0ed358a73 --- /dev/null +++ b/webui/scss/app/components/userlist/_user_state.scss @@ -0,0 +1,40 @@ +.tp__chip.tp__user-status { + width: 3.5rem; + min-width: 3.5rem; +} + +.tp__chip.tp__user-status__active { + color: var(--tag-active-color); + background-color: var(--tag-active-background-color); + border-color: var(--tag-active-border-color); +} + +.tp__chip.tp__user-status__expired { + color: var(--tag-status-expired-color); + background-color: var(--tag-status-expired-background-color); + border-color: var(--tag-status-expired-border-color); +} + +.tp__chip.tp__user-status__banned { + color: var(--tag-status-banned-color); + background-color: var(--tag-status-banned-background-color); + border-color: var(--tag-status-banned-border-color); +} + +.tp__chip.tp__user-status__trial { + color: var(--tag-status-trial-color); + background-color: var(--tag-status-trial-background-color); + border-color: var(--tag-status-trial-border-color); +} + +.tp__chip.tp__user-status__disabled { + color: var(--tag-inactive-color); + background-color: var(--tag-inactive-background-color); + border-color: var(--tag-inactive-border-color); +} + +.tp__chip.tp__user-status__pending { + color: var(--tag-status-pending-color); + background-color: var(--tag-status-pending-background-color); + border-color: var(--tag-status-pending-border-color); +} \ No newline at end of file diff --git a/webui/scss/app/components/userlist/_userlist_view.scss b/webui/scss/app/components/userlist/_userlist_view.scss index 90d7620e4..74cf091bf 100644 --- a/webui/scss/app/components/userlist/_userlist_view.scss +++ b/webui/scss/app/components/userlist/_userlist_view.scss @@ -1,3 +1,12 @@ .tp__userlist-view { -} \ No newline at end of file + .tp__userlist-list__user { + display: flex; + flex-flow: column; + flex: 1 1 auto; + max-width: 100%; + box-sizing: border-box; + overflow: hidden; + } + +} diff --git a/webui/src/app/components/chip.rs b/webui/src/app/components/chip.rs index e4cdfbc7e..4b56ad983 100644 --- a/webui/src/app/components/chip.rs +++ b/webui/src/app/components/chip.rs @@ -40,7 +40,7 @@ pub fn Chip(props: &ChipProps) -> Html { html! { - { &props.label } + { &props.label } if props.removable { {"×"} } diff --git a/webui/src/app/components/dashboard/dashboard_view.rs b/webui/src/app/components/dashboard/dashboard_view.rs index 6854d3d54..ba2b70795 100644 --- a/webui/src/app/components/dashboard/dashboard_view.rs +++ b/webui/src/app/components/dashboard/dashboard_view.rs @@ -1,43 +1,13 @@ -use gloo_timers::callback::Interval; -use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_i18n::use_translation; -use crate::app::components::{Card, DiscordActionCard, UserActionCard, VersionActionCard, DocumentationActionCard, StatusCard, IpinfoActionCard}; -use crate::hooks::use_service_context; +use crate::app::components::{Card, DiscordActionCard, UserActionCard, VersionActionCard, + DocumentationActionCard, IpinfoActionCard, GithubActionCard}; +use crate::app::context::StatusContext; #[function_component] pub fn DashboardView() -> Html { let translate = use_translation(); - let services = use_service_context(); - let status = use_state(|| None); - - { - let services_ctx = services.clone(); - let status_signal = status.clone(); - - use_effect_with((), move |_| { - let fetch_status = { - let status = status_signal.clone(); - let services_ctx = services_ctx.clone(); - move || { - let status = status.clone(); - let services_ctx = services_ctx.clone(); - spawn_local(async move { - status.set(services_ctx.status.get_server_status().await.ok()); - }); - } - }; - - fetch_status(); - // all 5 seconds - let interval = Interval::new(5000, move || { - fetch_status(); - }); - - // Cleanup function - || drop(interval) - }); - } + let status_ctx = use_context::().expect("Status context not found"); html! {
@@ -47,58 +17,21 @@ pub fn DashboardView() -> Html {
- + +
-
- - - - - { - match &*status { - Some(stats) => { - if let Some(map) = &stats.active_provider_connections { - let cards = map.iter().map(|(provider, connections)| { - html! { - - - - } - }).collect::(); - - cards - } else { - html! { - - - - } - } - } - None => html! { - - - - } - } - } -
+ //
+ // + // + //
} diff --git a/webui/src/app/components/dashboard/github_action_card.rs b/webui/src/app/components/dashboard/github_action_card.rs new file mode 100644 index 000000000..b59e83d02 --- /dev/null +++ b/webui/src/app/components/dashboard/github_action_card.rs @@ -0,0 +1,33 @@ +use gloo_utils::window; +use crate::app::components::{ActionCard, TextButton}; +use crate::hooks::use_service_context; +use yew::prelude::*; +use yew_i18n::use_translation; + + +#[function_component] +pub fn GithubActionCard() -> Html { + let services = use_service_context(); + let translate = use_translation(); + + + let handle_url = { + let mut github_link = services.config.ui_config.github.to_string(); + if github_link.is_empty() { + github_link = String::from("https://github.com/euzu/tuliprox"); + } + Callback::from(move |_| { + let _ = window().open_with_url_and_target( + &github_link, + "_blank", + ); + }) + }; + + html! { + + + + } +} \ No newline at end of file diff --git a/webui/src/app/components/dashboard/mod.rs b/webui/src/app/components/dashboard/mod.rs index fe55b4733..eb85365af 100644 --- a/webui/src/app/components/dashboard/mod.rs +++ b/webui/src/app/components/dashboard/mod.rs @@ -3,9 +3,11 @@ mod user_action_card; mod version_action_card; mod discord_action_card; mod documentation_action_card; +mod github_action_card; mod status_card; mod ipinfo_action_card; -pub mod dashboard_view; +mod dashboard_view; +mod stats_view; pub use self::action_card::*; pub use self::status_card::*; @@ -13,4 +15,7 @@ pub use self::user_action_card::*; pub use self::version_action_card::*; pub use self::discord_action_card::*; pub use self::documentation_action_card::*; -pub use self::ipinfo_action_card::*; \ No newline at end of file +pub use self::ipinfo_action_card::*; +pub use self::github_action_card::*; +pub use self::dashboard_view::*; +pub use self::stats_view::*; diff --git a/webui/src/app/components/dashboard/stats_view.rs b/webui/src/app/components/dashboard/stats_view.rs new file mode 100644 index 000000000..a1414d8fe --- /dev/null +++ b/webui/src/app/components/dashboard/stats_view.rs @@ -0,0 +1,67 @@ +use yew::prelude::*; +use yew_i18n::use_translation; +use crate::app::components::{Card, StatusCard, StatusContext}; + +#[function_component] +pub fn StatsView() -> Html { + let translate = use_translation(); + let status_ctx = use_context::().expect("Status context not found"); + + let render_active_provider_connections = || -> Html { + match &status_ctx.status { + Some(stats) => { + if let Some(map) = &stats.active_provider_connections { + let cards = map.iter().map(|(provider, connections)| { + html! { + + + + } + }).collect::(); + + cards + } else { + html! { + + + + } + } + } + None => html! { + + + + } + } + }; + + html! { +
+
+

{ translate.t("LABEL.STATS")}

+
+
+
+ + +
+
+ + + { render_active_provider_connections() } +
+
+
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/home.rs b/webui/src/app/components/home.rs index 8e48bf62c..f2a1f78a2 100644 --- a/webui/src/app/components/home.rs +++ b/webui/src/app/components/home.rs @@ -1,10 +1,12 @@ use std::future; use std::rc::Rc; +use gloo_timers::callback::Interval; +use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew::suspense::use_future; -use shared::model::AppConfigDto; -use crate::app::components::{IconButton, Sidebar, DashboardView, PlaylistView, Panel, UserlistView}; -use crate::app::context::ConfigContext; +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_service_context; @@ -13,6 +15,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 config = use_state(|| None::>); + let status = use_state(|| None::>); let view_visible = use_state(|| ViewType::Users); @@ -47,15 +50,49 @@ pub fn Home() -> Html { }); } - let context = ConfigContext { + { + let services_ctx = services.clone(); + let status_signal = status.clone(); + + use_effect_with((), move |_| { + let fetch_status = { + let status = status_signal.clone(); + let services_ctx = services_ctx.clone(); + move || { + let status = status.clone(); + let services_ctx = services_ctx.clone(); + spawn_local(async move { + status.set(services_ctx.status.get_server_status().await.ok()); + }); + } + }; + + fetch_status(); + // all 5 seconds + let interval = Interval::new(5000, move || { + fetch_status(); + }); + + // Cleanup function + || drop(interval) + }); + } + + + let config_context = ConfigContext { config: (*config).clone(), }; + let status_context = StatusContext { + status: (*status).clone(), + }; + //
//
html! { - context={context}> + context={config_context}> + context={status_context}>
@@ -80,9 +117,12 @@ pub fn Home() -> Html {
- + + + + @@ -92,6 +132,7 @@ pub fn Home() -> Html {
+
>
> } } diff --git a/webui/src/app/components/mod.rs b/webui/src/app/components/mod.rs index 87444e4c3..214c20797 100644 --- a/webui/src/app/components/mod.rs +++ b/webui/src/app/components/mod.rs @@ -38,7 +38,6 @@ pub use self::login::*; pub use self::home::*; pub use self::sidebar::*; pub use self::preferences::*; -pub use dashboard::dashboard_view::*; pub use self::dashboard::*; pub use self::breadcrumbs::*; pub use self::playlist::*; diff --git a/webui/src/app/components/sidebar.rs b/webui/src/app/components/sidebar.rs index a1341d017..12b2a98c3 100644 --- a/webui/src/app/components/sidebar.rs +++ b/webui/src/app/components/sidebar.rs @@ -40,14 +40,16 @@ pub fn Sidebar(props: &SidebarProps) -> Html {
- + - -
diff --git a/webui/src/app/components/userlist/create.rs b/webui/src/app/components/userlist/edit.rs similarity index 67% rename from webui/src/app/components/userlist/create.rs rename to webui/src/app/components/userlist/edit.rs index da6f48891..c53edc098 100644 --- a/webui/src/app/components/userlist/create.rs +++ b/webui/src/app/components/userlist/edit.rs @@ -3,7 +3,7 @@ use yew_i18n::use_translation; use crate::app::components::{UserlistContext, UserlistPage, TextButton}; #[function_component] -pub fn UserlistCreate() -> Html { +pub fn UserEdit() -> Html { let translate = use_translation(); let userlist_ctx = use_context::().expect("Userlist context not found"); @@ -15,15 +15,15 @@ pub fn UserlistCreate() -> Html { }; html! { -
-
-

{ translate.t("LABEL.CREATE")}

+
+
+

{ translate.t( if userlist_ctx.selected_user.is_none() { "LABEL.CREATE" } else { "LABEL.EDIT" } )}

-
+
} diff --git a/webui/src/app/components/userlist/list.rs b/webui/src/app/components/userlist/list.rs index 46a43a220..6487e0986 100644 --- a/webui/src/app/components/userlist/list.rs +++ b/webui/src/app/components/userlist/list.rs @@ -11,7 +11,7 @@ pub fn UserlistList() -> Html { let handle_create = { let userlist_ctx = userlist_ctx.clone(); Callback::from(move |_| { - userlist_ctx.active_page.set(UserlistPage::Create); + userlist_ctx.active_page.set(UserlistPage::Edit); }) }; @@ -30,11 +30,11 @@ pub fn UserlistList() -> Html {

{ translate.t("LABEL.USERS")}

-
+
{ userlist_body }
diff --git a/webui/src/app/components/userlist/max_connections.rs b/webui/src/app/components/userlist/max_connections.rs new file mode 100644 index 000000000..e3d1cb4e3 --- /dev/null +++ b/webui/src/app/components/userlist/max_connections.rs @@ -0,0 +1,17 @@ +use yew::prelude::*; +use crate::app::components::{AppIcon}; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct MaxConnectionsProps { + pub value: u32, +} + +#[function_component] +pub fn MaxConnections(props: &MaxConnectionsProps) -> Html { + + if props.value <= 0 { + html! { } + } else { + html! { { props.value } } + } +} \ No newline at end of file diff --git a/webui/src/app/components/userlist/mod.rs b/webui/src/app/components/userlist/mod.rs index 13f1bad5c..1acb86ef2 100644 --- a/webui/src/app/components/userlist/mod.rs +++ b/webui/src/app/components/userlist/mod.rs @@ -1,10 +1,14 @@ mod page; mod userlist_view; mod list; -mod create; +mod edit; mod user_table; mod proxy_type_view; +mod user_status; +mod max_connections; pub use self::userlist_view::*; pub use self::page::*; -pub use self::proxy_type_view::*; \ No newline at end of file +pub use self::proxy_type_view::*; +pub use self::user_status::*; +pub use self::max_connections::*; \ No newline at end of file diff --git a/webui/src/app/components/userlist/page.rs b/webui/src/app/components/userlist/page.rs index 404102da8..82fe97436 100644 --- a/webui/src/app/components/userlist/page.rs +++ b/webui/src/app/components/userlist/page.rs @@ -6,7 +6,7 @@ use shared::info_err; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum UserlistPage { List, - Create, + Edit, } impl FromStr for UserlistPage { @@ -15,7 +15,7 @@ impl FromStr for UserlistPage { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "list" => Ok(UserlistPage::List), - "create" => Ok(UserlistPage::Create), + "edit" => Ok(UserlistPage::Edit), _ => Err(info_err!(format!("Unknown page type: {s}"))), } } @@ -25,7 +25,7 @@ impl Display for UserlistPage { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", match *self { Self::List => "list", - Self::Create => "create", + Self::Edit => "edit", }) } } \ No newline at end of file diff --git a/webui/src/app/components/userlist/proxy_type_view.rs b/webui/src/app/components/userlist/proxy_type_view.rs index 64aec9105..233b9d3fe 100644 --- a/webui/src/app/components/userlist/proxy_type_view.rs +++ b/webui/src/app/components/userlist/proxy_type_view.rs @@ -1,10 +1,8 @@ -use log::info; -use crate::app::components::{convert_bool_to_chip_style, Chip}; +use crate::app::components::{Chip}; use shared::model::{ClusterFlags, ProxyType}; use yew::prelude::*; use yew_i18n::use_translation; - #[derive(Properties, Clone, PartialEq, Debug)] pub struct ProxyTypeViewProps { pub value: ProxyType, @@ -13,17 +11,36 @@ pub struct ProxyTypeViewProps { #[function_component] pub fn ProxyTypeView(props: &ProxyTypeViewProps) -> Html { let translate = use_translation(); + + let render_chip = |flag: bool, class_sfx: &str, label: &str, | -> Html { + if flag { + html! { + + } + } else { + html!{ + + } + } + }; + match props.value { ProxyType::Reverse(flags) => { let cluster = flags.unwrap_or_else(ClusterFlags::all); - info!("{cluster:?}"); - html! { - <> - - - - - } + let live_flag = cluster.contains(ClusterFlags::Live); + let vod_flag = cluster.contains(ClusterFlags::Vod); + let series_flag = cluster.contains(ClusterFlags::Series); + if live_flag && vod_flag && series_flag { + html! { } + } else { + html! { +
+ { render_chip(live_flag, "live", "LABEL.LIVE") } + { render_chip(vod_flag, "vod", "LABEL.VOD") } + { render_chip(series_flag, "series", "LABEL.SERIES" ) } +
+ } + } } ProxyType::Redirect => html! { diff --git a/webui/src/app/components/userlist/user_status.rs b/webui/src/app/components/userlist/user_status.rs new file mode 100644 index 000000000..f59a497ea --- /dev/null +++ b/webui/src/app/components/userlist/user_status.rs @@ -0,0 +1,24 @@ +use shared::model::{ProxyUserStatus}; +use yew::prelude::*; +use yew_i18n::use_translation; +use crate::app::components::Chip; + +fn convert_status_to_chip_style(status: &ProxyUserStatus) -> String { + format!("tp__user-status tp__user-status__{}", status.to_string().to_lowercase()) +} + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct UserStatusProps { + pub status: Option, +} + +#[function_component] +pub fn UserStatus(props: &UserStatusProps) -> Html { + match props.status.as_ref() { + Some(status) => html! { + + }, + None => html! {}, + } +} \ No newline at end of file diff --git a/webui/src/app/components/userlist/user_table.rs b/webui/src/app/components/userlist/user_table.rs index e0b2e9f65..4c375a42a 100644 --- a/webui/src/app/components/userlist/user_table.rs +++ b/webui/src/app/components/userlist/user_table.rs @@ -1,7 +1,8 @@ use crate::app::components::menu_item::MenuItem; use crate::app::components::popup_menu::PopupMenu; -use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, HideContent, ProxyTypeView, Table, TableDefinition}; -use crate::hooks::use_service_context; +use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, HideContent, MaxConnections, + ProxyTypeView, RevealContent, Table, TableDefinition, UserStatus, + UserlistContext, UserlistPage}; use crate::model::DialogResult; use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind}; use std::fmt::Display; @@ -10,24 +11,25 @@ use std::str::FromStr; use yew::platform::spawn_local; use yew::prelude::*; use yew_i18n::use_translation; +use shared::utils::{unix_ts_to_str, Substring}; use crate::app::context::TargetUser; use crate::services::DialogService; const HEADERS: [&str; 15] = [ "TABLE.EMPTY", "TABLE.ENABLED", + "TABLE.STATUS", "TABLE.PLAYLIST", "TABLE.USERNAME", "TABLE.PASSWORD", "TABLE.TOKEN", "TABLE.PROXY", "TABLE.SERVER", + "TABLE.MAX_CONNECTIONS", + "TABLE.UI_ENABLED", "TABLE.EPG_TIMESHIFT", "TABLE.CREATED_AT", "TABLE.EXP_DATE", - "TABLE.MAX_CONNECTIONS", - "TABLE.STATUS", - "TABLE.UI_ENABLED", "TABLE.COMMENT", ]; @@ -39,8 +41,8 @@ pub struct UserTableProps { #[function_component] pub fn UserTable(props: &UserTableProps) -> Html { let translate = use_translation(); - let services = use_service_context(); let dialog = use_context::().expect("Dialog service not found"); + let userlist_context = use_context::().expect("Userlist context not found"); let popup_anchor_ref = use_state(|| None::); let popup_is_open = use_state(|| false); let selected_dto = use_state(|| None::>); @@ -98,19 +100,27 @@ pub fn UserTable(props: &UserTableProps) -> Html { } } 1 => html! { }, - 2 => html! { dto.target.as_str() }, - 3 => html! { dto.credentials.username.as_str() }, - 4 => html! { }, - 5 => html! { dto.credentials.token.as_ref().map_or_else(|| html!{}, |token| html! { }) }, - 6 => html! { }, - 7 => dto.credentials.server.as_ref().map_or_else(|| html! {}, |s| html! { s } ), - // 6 => dto.t_filter.as_ref().map_or_else(|| html! {}, |f| html! { })}> }), - // 7 => dto.rename.as_ref().map_or_else(|| html! {}, |_r| html! { }), - // 8 => html! { }, - // 9 => html! { }, - // 10 => html! { }, + 2 => html! { }, + 3 => html! { dto.target.as_str() }, + 4 => html! { dto.credentials.username.as_str() }, + 5 => html! { }, + 6 => html! { dto.credentials.token.as_ref().map_or_else(|| html!{}, |token| html! { }) }, + 7 => html! { }, + 8 => dto.credentials.server.as_ref().map_or_else(|| html! {}, |s| html! { s } ), + 9 => html! { }, + 10 => html! { }, + 11 => dto.credentials.epg_timeshift.as_ref().map_or_else(|| html! {}, |s| html! { s } ), + 12 => dto.credentials.created_at.as_ref().and_then(|ts| unix_ts_to_str(*ts)) + .map(|s| html! { { s } }).unwrap_or_else(|| html! { }), + 13 => dto.credentials.exp_date.as_ref().and_then(|ts| unix_ts_to_str(*ts)) + .map(|s| html! { { s } }).unwrap_or_else(|| html! { }), + 14 => dto.credentials.comment.as_ref() + .map_or_else(|| html!{}, + |comment|html! { {comment} }), _ => html! {""}, } }) @@ -138,10 +148,16 @@ pub fn UserTable(props: &UserTableProps) -> Html { let confirm = dialog.clone(); let translate = translate.clone(); let selected_dto = selected_dto.clone(); + let ul_context = userlist_context.clone(); Callback::from(move |name: String| { if let Ok(action) = TableAction::from_str(&name) { match action { - TableAction::Edit => {} + TableAction::Edit => { + if let Some(dto) = &*selected_dto { + ul_context.selected_user.set(Some(Rc::clone(dto))); + ul_context.active_page.set(UserlistPage::Edit); + } + } TableAction::Refresh => { } TableAction::Delete => { diff --git a/webui/src/app/components/userlist/userlist_view.rs b/webui/src/app/components/userlist/userlist_view.rs index d84417b0c..8e49d0fb7 100644 --- a/webui/src/app/components/userlist/userlist_view.rs +++ b/webui/src/app/components/userlist/userlist_view.rs @@ -1,4 +1,4 @@ -use crate::app::components::userlist::create::UserlistCreate; +use crate::app::components::userlist::edit::UserEdit; use crate::app::components::userlist::list::UserlistList; use crate::app::components::userlist::page::UserlistPage; use crate::app::components::{Breadcrumbs, Panel, TargetUser}; @@ -11,15 +11,41 @@ use yew_i18n::use_translation; pub fn UserlistView() -> Html { let translate = use_translation(); let breadcrumbs = use_state(|| Rc::new(vec![translate.t("LABEL.USERLIST"), translate.t("LABEL.LIST")])); - let active_page = use_state(|| UserlistPage::List); + let selected_user= use_state(|| None::>); let config_ctx = use_context::().expect("Config context not found"); + let users = use_memo(config_ctx.config.as_ref().and_then(|c| c.api_proxy.clone()), + |api_cfg_opt| { + if let Some(api_cfg) = api_cfg_opt { + let mut users = Vec::new(); + for target in &api_cfg.user { + for creds in &target.credentials { + users.push(Rc::new(TargetUser { + target: target.target.to_string(), + credentials: Rc::new(creds.clone()), + })); + } + } + Some(Rc::new(users)) + } else { + None + } + }); + + let userlist_context = UserlistContext { + selected_user: selected_user.clone(), + users, + active_page: active_page.clone(), + }; + let handle_breadcrumb_select = { let view_visible = active_page.clone(); + let selected_user = selected_user.clone(); Callback::from(move |(_name, index)| { if index == 0 && *view_visible != UserlistPage::List { + selected_user.set(None); view_visible.set(UserlistPage::List); } }) @@ -29,52 +55,31 @@ pub fn UserlistView() -> Html { let breadcrumbs = breadcrumbs.clone(); let view_visible_dep = active_page.clone(); let view_visible = active_page.clone(); + let selected_user_dep = selected_user.clone(); + let selected_user = selected_user.clone(); let translate = translate.clone(); - use_effect_with(view_visible_dep, move |_| { + use_effect_with((view_visible_dep, selected_user_dep), move |_| { match *view_visible { UserlistPage::List => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.USERS"), translate.t("LABEL.LIST")])), - UserlistPage::Create => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.USERS"), translate.t("LABEL.CREATE")])), + UserlistPage::Edit => breadcrumbs.set(Rc::new(vec![translate.t("LABEL.USERS"), translate.t( if selected_user.is_none() { "LABEL.CREATE" } else {"LABEL.EDIT" })])), } }); }; - let users = use_memo(config_ctx.config.as_ref().and_then(|c| c.api_proxy.clone()), - |api_cfg_opt| { - if let Some(api_cfg) = api_cfg_opt { - let mut users = Vec::new(); - for target in &api_cfg.user { - for creds in &target.credentials { - users.push(Rc::new(TargetUser { - target: target.target.to_string(), - credentials: Rc::new(creds.clone()), - })); - } - } - Some(Rc::new(users)) - } else { - None - } - }); - let handle_create = { Callback::from(move |cmd: String| {}) }; - let context = UserlistContext { - users, - active_page: active_page.clone(), - }; - html! { - context={context}> + context={userlist_context}>
- - + +
diff --git a/webui/src/app/context.rs b/webui/src/app/context.rs index 19fe2d2e3..596ec4a25 100644 --- a/webui/src/app/context.rs +++ b/webui/src/app/context.rs @@ -1,6 +1,6 @@ use std::rc::Rc; use yew::UseStateHandle; -use shared::model::{AppConfigDto, ConfigTargetDto, ProxyUserCredentialsDto}; +use shared::model::{AppConfigDto, ConfigTargetDto, ProxyUserCredentialsDto, StatusCheck}; use crate::app::components::{InputRow, PlaylistPage, UserlistPage}; #[derive(Clone, PartialEq)] @@ -17,6 +17,7 @@ pub struct TargetUser { #[derive(Clone, PartialEq)] pub struct UserlistContext { + pub selected_user: UseStateHandle>>, pub users: Rc>>>>, pub active_page: UseStateHandle, } @@ -24,4 +25,9 @@ pub struct UserlistContext { #[derive(Clone, PartialEq)] pub struct ConfigContext { pub config: Option>, -} \ No newline at end of file +} + +#[derive(Clone, PartialEq)] +pub struct StatusContext { + pub status: Option>, +} diff --git a/webui/src/model/view_type.rs b/webui/src/model/view_type.rs index 41bc87d86..14f275752 100644 --- a/webui/src/model/view_type.rs +++ b/webui/src/model/view_type.rs @@ -5,6 +5,7 @@ use shared::error::{info_err, TuliproxError, TuliproxErrorKind}; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ViewType { Dashboard, + Stats, Users, Playlists, } @@ -15,6 +16,7 @@ impl FromStr for ViewType { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "dashboard" => Ok(ViewType::Dashboard), + "stats" => Ok(ViewType::Stats), "users" => Ok(ViewType::Users), "playlists" => Ok(ViewType::Playlists), _ => Err(info_err!(format!("Unknown view type: {s}"))), @@ -26,6 +28,7 @@ impl fmt::Display for ViewType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { ViewType::Dashboard => "dashboard", + ViewType::Stats => "stats", ViewType::Users => "users", ViewType::Playlists => "playlists", }; diff --git a/webui/src/model/web_config.rs b/webui/src/model/web_config.rs index a28ccf8ed..6952d09b0 100644 --- a/webui/src/model/web_config.rs +++ b/webui/src/model/web_config.rs @@ -21,6 +21,8 @@ pub struct WebConfig { #[serde(default)] pub discord: String, #[serde(default)] + pub github: String, + #[serde(default)] pub documentation: String, #[serde(alias = "wsUrl")] pub ws_url: String, diff --git a/webui/src/services/dialog_service.rs b/webui/src/services/dialog_service.rs index d06f502dc..2b2b8a4c5 100644 --- a/webui/src/services/dialog_service.rs +++ b/webui/src/services/dialog_service.rs @@ -86,7 +86,7 @@ impl DialogService { let (future, resolver) = DialogFuture::new(); let request = ConfirmRequest { title: title.to_string(), - ok_caption: "LABEL.OK".into(), + ok_caption: "LABEL.YES".into(), cancel_caption: "LABEL.CANCEL".into(), resolve: Rc::new(RefCell::new(Some(Box::new(resolver)))), }; diff --git a/webui/src/services/status_service.rs b/webui/src/services/status_service.rs index e73af91aa..a8586f075 100644 --- a/webui/src/services/status_service.rs +++ b/webui/src/services/status_service.rs @@ -1,3 +1,4 @@ +use std::rc::Rc; use crate::services::request_get; use shared::model::StatusCheck; @@ -17,7 +18,7 @@ impl StatusService { Self {} } - pub async fn get_server_status(&self) -> Result { - request_get::(STATUS_PATH).await + pub async fn get_server_status(&self) -> Result, crate::error::Error> { + request_get::>(STATUS_PATH).await } } \ No newline at end of file