diff --git a/.gitignore b/.gitignore index bcff151f2..6ec51ca70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,196 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,rust +# Edit at https://www.toptal.com/developers/gitignore?templates=python,rust + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# End of https://www.toptal.com/developers/gitignore/api/python,rust + +AGENTS.md .DS_Store node_modules dist diff --git a/extensions/cli/ccfddl/convert_to_ical.py b/extensions/cli/ccfddl/convert_to_ical.py index 72fb2d882..7e250e054 100644 --- a/extensions/cli/ccfddl/convert_to_ical.py +++ b/extensions/cli/ccfddl/convert_to_ical.py @@ -209,30 +209,38 @@ def convert_to_ical( f.write(cal.to_ical()) +def add_index_entry(index, key: str, file_path: str): + index[key].add(file_path) + + def reverse_index(file_paths: list[str], subs: list[str]): - index = defaultdict(list) + index = defaultdict(set) for file_path in file_paths: with open(file_path, "r", encoding="utf-8") as f: conferences = yaml.safe_load(f) for conf_data in conferences: - # title = conf_data['title'] sub = conf_data["sub"] rank = conf_data["rank"] - - # index[title].append(file_path) + ccf_rank = rank.get("ccf", "N") + core_rank = rank.get("core", "N") + thcpl_rank = rank.get("thcpl", "N") rank_keys = [ - "ccf_" + rank.get("ccf", "N"), - "core_" + rank.get("core", "N"), - "thcpl_" + rank.get("thcpl", "N"), + f"ccf_{ccf_rank}", + f"core_{core_rank}", + f"thcpl_{thcpl_rank}", ] - for key in rank_keys: - index[key].append(file_path) - index[key + "_" + sub].append(file_path) - index[sub].append(file_path) - return index + add_index_entry(index, sub, file_path) + + for size in range(1, len(rank_keys) + 1): + for combo in combinations(rank_keys, size): + key = "_".join(combo) + add_index_entry(index, key, file_path) + add_index_entry(index, f"{key}_{sub}", file_path) + + return {key: sorted(paths) for key, paths in index.items()} if __name__ == "__main__": diff --git a/extensions/ical/readme.md b/extensions/ical/readme.md index 889b247f0..1d870182a 100644 --- a/extensions/ical/readme.md +++ b/extensions/ical/readme.md @@ -7,8 +7,12 @@ The filter is mapped to the name of iCal file in the following rules: -- one filter: `deadlines_en.ics` and `deadlines_zh.ics` -- two filters: `deadlines_{lang}_{rank}.ics` and `deadlines_{lang}_{sub}.ics` -- common filters: `deadlines_{lang}_{rank}_{sub}.ics` +- no filter: `deadlines_en.ics` and `deadlines_zh.ics` +- one filter: `deadlines_{lang}_{ccf_rank}.ics`, `deadlines_{lang}_{core_rank}.ics`, `deadlines_{lang}_{thcpl_rank}.ics`, or `deadlines_{lang}_{sub}.ics` +- two filters: any ordered pair among `ccf_rank`, `core_rank`, `thcpl_rank`, and `sub` +- three filters: any ordered triple among `ccf_rank`, `core_rank`, `thcpl_rank`, and `sub` +- four filters: `deadlines_{lang}_{ccf_rank}_{core_rank}_{thcpl_rank}_{sub}.ics` -For example, given filter: lang=zh, sub=AI,CG, ccf=A,thcpl=A, then it will refer to `deadlines_zh_ccf_A_AI.ics`, `deadlines_zh_ccf_A_CG.ics`, `deadlines_zh_thcpl_A_AI.ics` and `deadlines_zh_thcpl_A_CG.ics`. +For example, given filter: lang=en, core=A, thcpl=B, sub=SE, it will refer to `deadlines_en_core_A_thcpl_B_SE.ics`. + +For `A*`, the generated filename uses `Astar`, for example `deadlines_en_core_Astar_SE.ics`. diff --git a/src/components/calendar_popover.rs b/src/components/calendar_popover.rs index 798b736bc..1f41e930f 100644 --- a/src/components/calendar_popover.rs +++ b/src/components/calendar_popover.rs @@ -1,13 +1,11 @@ use leptos::prelude::*; -use leptos::*; use thaw::*; -use chrono::{DateTime, Utc}; #[component] pub fn CalendarPopover( google_calendar_url: Option, icloud_calendar_url: Option, - is_mobile: RwSignal + is_mobile: RwSignal, ) -> impl IntoView { let show_popover = RwSignal::new(false); diff --git a/src/components/checkbox_button.rs b/src/components/checkbox_button.rs index 653075bd1..142b7df88 100644 --- a/src/components/checkbox_button.rs +++ b/src/components/checkbox_button.rs @@ -1,147 +1,355 @@ -use leptos::*; use leptos::prelude::*; use std::collections::HashSet; +const NON_RANK_VALUE: &str = "N"; + +#[derive(Clone, PartialEq, Eq)] +pub struct FilterDropdownOption { + pub value: &'static str, + pub label: &'static str, + pub summary_label: &'static str, +} + +pub fn normalize_rank_filter_selection(selected_values: &mut HashSet) { + if selected_values.contains(NON_RANK_VALUE) && selected_values.len() > 1 { + selected_values.remove(NON_RANK_VALUE); + } +} + +pub fn ccf_filter_options() -> Vec { + vec![ + FilterDropdownOption { + value: "A", + label: "CCF A", + summary_label: "A", + }, + FilterDropdownOption { + value: "B", + label: "CCF B", + summary_label: "B", + }, + FilterDropdownOption { + value: "C", + label: "CCF C", + summary_label: "C", + }, + FilterDropdownOption { + value: "N", + label: "Non-CCF", + summary_label: "Non", + }, + ] +} + +pub fn core_filter_options() -> Vec { + vec![ + FilterDropdownOption { + value: "A*", + label: "CORE A*", + summary_label: "A*", + }, + FilterDropdownOption { + value: "A", + label: "CORE A", + summary_label: "A", + }, + FilterDropdownOption { + value: "B", + label: "CORE B", + summary_label: "B", + }, + FilterDropdownOption { + value: "C", + label: "CORE C", + summary_label: "C", + }, + FilterDropdownOption { + value: "N", + label: "Non-CORE", + summary_label: "Non", + }, + ] +} + +pub fn thcpl_filter_options() -> Vec { + vec![ + FilterDropdownOption { + value: "A", + label: "THCPL A", + summary_label: "A", + }, + FilterDropdownOption { + value: "B", + label: "THCPL B", + summary_label: "B", + }, + FilterDropdownOption { + value: "N", + label: "Non-THCPL", + summary_label: "Non", + }, + ] +} + #[component] -pub fn CheckboxButton( - label: String, - value: String, - rank_list: RwSignal>, - #[prop(optional)] is_first: bool, - #[prop(optional)] is_last: bool, +pub fn MultiSelectDropdown( + dropdown_id: String, + title: String, + options: Vec, + selected_values: RwSignal>, + use_english: RwSignal, + panel_width: String, + open_dropdown: RwSignal>, ) -> impl IntoView { - let value_clone = value.clone(); - let is_checked = Memo::new(move |_| { - rank_list.get().contains(&value_clone) + let dropdown_id_for_open = dropdown_id.clone(); + let is_open = + Memo::new(move |_| open_dropdown.get().as_deref() == Some(dropdown_id_for_open.as_str())); + let options_for_summary = options.clone(); + let title_for_summary = title.clone(); + let title_for_panel = title.clone(); + let options_for_render = StoredValue::new(options.clone()); + let summary = Memo::new(move |_| { + let selected = selected_values.get(); + let selected_labels: Vec<&str> = options_for_summary + .iter() + .filter(|option| selected.contains(option.value)) + .map(|option| option.summary_label) + .collect(); + + match selected_labels.len() { + 0 => title_for_summary.clone(), + 1 => format!("{title_for_summary} {}", selected_labels[0]), + 2 => format!( + "{title_for_summary} {},{}", + selected_labels[0], selected_labels[1] + ), + _ => format!( + "{title_for_summary} {},{}+{}", + selected_labels[0], + selected_labels[1], + selected_labels.len() - 2 + ), + } }); - let handle_change = move |_ev| { - let value_for_update = value.clone(); - rank_list.update(|set| { - if set.contains(&value_for_update) { - set.remove(&value_for_update); - } else { - set.insert(value_for_update); - } - }); - }; + let has_selection = Memo::new(move |_| !selected_values.get().is_empty()); + let clear_label = Memo::new(move |_| { + if use_english.get() { + "Clear".to_string() + } else { + "清空".to_string() + } + }); + let dropdown_id_for_toggle = dropdown_id.clone(); view! { - +
+ + + +
+
+ {title_for_panel.clone()} + +
+ +
+ {move || { + options_for_render + .get_value() + .into_iter() + .map(|option| { + let value = option.value.to_string(); + let label = option.label.to_string(); + let value_for_checked = value.clone(); + let value_for_update = value.clone(); + + view! { + + } + }) + .collect_view() + }} +
+
+
+
+ - } -} -#[component] -pub fn CheckboxButtonGroup( - rank_list: RwSignal>, -) -> impl IntoView { + .filter-dropdown-options { + display: flex; + flex-direction: column; + gap: 4px; + } - view! { -
-
- - - - -
-
+ .filter-dropdown-option { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 6px; + border-radius: 6px; + color: #334155; + font-size: 12px; + cursor: pointer; + } + + .filter-dropdown-option:hover { + background: #f8fafc; + } + + .filter-dropdown-option input { + margin: 0; + } + "#} + } } diff --git a/src/components/conf.rs b/src/components/conf.rs index ea54d3f32..abf7d88b6 100644 --- a/src/components/conf.rs +++ b/src/components/conf.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use chrono::prelude::*; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct Conference { @@ -42,7 +42,7 @@ pub struct AccYear { pub accepted: i32, pub str: String, pub rate: String, - pub source: Option + pub source: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -94,10 +94,12 @@ pub struct ConfItem { pub google_calendar_url: Option, pub icloud_calendar_url: Option, pub acc_str: Option, - pub ddls: Vec + pub ddls: Vec, } -pub async fn fetch_all_conf(base_url: &String) -> Result, Box> { +pub async fn fetch_all_conf( + base_url: &String, +) -> Result, Box> { let url = format!("{}/conference/allconf.yml", base_url); let response = reqwest::get(url).await?; let contents = response.text().await?; @@ -106,7 +108,9 @@ pub async fn fetch_all_conf(base_url: &String) -> Result, Box Result, Box> { +pub async fn fetch_all_acc( + base_url: &String, +) -> Result, Box> { let url = format!("{}/conference/allacc.yml", base_url); let response = reqwest::get(url).await?; let contents = response.text().await?; diff --git a/src/components/countdown.rs b/src/components/countdown.rs index 463dde75b..2c5a90031 100644 --- a/src/components/countdown.rs +++ b/src/components/countdown.rs @@ -49,10 +49,7 @@ where prev_handle.clear(); } - set_interval_with_handle( - f.clone(), - Duration::from_millis(interval_millis.get()), - ) - .expect("could not create interval") + set_interval_with_handle(f.clone(), Duration::from_millis(interval_millis.get())) + .expect("could not create interval") }); } diff --git a/src/components/header.rs b/src/components/header.rs index a07843f7c..ebcf4833e 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -1,7 +1,6 @@ use leptos::prelude::*; use serde::{Deserialize, Serialize}; use wasm_bindgen_futures::spawn_local; -use web_sys::{console, js_sys, window}; use crate::components::gitbutton::GitButton; @@ -28,9 +27,7 @@ pub fn Header() -> impl IntoView { set_show_latest_conf.set(show_conf); set_show_str.set(conf_str); } - Err(_) => { - - } + Err(_) => {} } }); }); diff --git a/src/components/mod.rs b/src/components/mod.rs index 6016613ff..e2add5a32 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,10 +1,10 @@ +pub mod calendar_popover; +pub mod checkbox_button; +pub mod conf; +pub mod countdown; pub mod gitbutton; pub mod header; -pub mod timeline; pub mod showtable; -pub mod countdown; -pub mod timezone; -pub mod checkbox_button; -pub mod calendar_popover; pub mod subscription_modal; -pub mod conf; +pub mod timeline; +pub mod timezone; diff --git a/src/components/showtable.rs b/src/components/showtable.rs index 4e0e225e9..5d5c3deed 100644 --- a/src/components/showtable.rs +++ b/src/components/showtable.rs @@ -1,21 +1,21 @@ +use crate::components::calendar_popover::*; use crate::components::checkbox_button::*; use crate::components::conf::ConfItem; use crate::components::conf::*; use crate::components::countdown::CountDown; +use crate::components::subscription_modal::*; use crate::components::timeline::*; use crate::components::timezone::*; -use crate::components::calendar_popover::*; -use crate::components::subscription_modal::*; -use chrono::{DateTime, FixedOffset, Utc}; +use chrono::{DateTime, FixedOffset}; use leptos::prelude::*; use serde_json; use std::collections::HashMap; use std::collections::HashSet; use thaw::*; +use urlencoding::encode; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{console, js_sys, window}; -use urlencoding::encode; +use web_sys::{console, window}; #[component] pub fn ShowTable() -> impl IntoView { @@ -24,16 +24,18 @@ pub fn ShowTable() -> impl IntoView { // switch let cached_use_english = get_from_local_storage("use_english"); - let use_english = RwSignal::new(cached_use_english - .as_deref() - .and_then(|s| s.parse::().ok()) - .unwrap_or(false)); + let use_english = RwSignal::new( + cached_use_english + .as_deref() + .and_then(|s| s.parse::().ok()) + .unwrap_or(false), + ); // checkbox let sub_list = RwSignal::new(get_categories()); let cached_check_list: HashSet = get_from_local_storage("types") - .and_then(|data| serde_json::from_str(&data).ok()) - .unwrap_or_else(|| HashSet::new()); + .and_then(|data| serde_json::from_str(&data).ok()) + .unwrap_or_else(|| HashSet::new()); let check_list = RwSignal::new(cached_check_list); let is_all_checked_memo = Memo::new(move |_| { let total_count = sub_list.get().len(); @@ -64,15 +66,27 @@ pub fn ShowTable() -> impl IntoView { let input_value = RwSignal::new(String::new()); // checkboxbutton - let cached_rank_list: HashSet = get_from_local_storage("ranks") - .and_then(|data| serde_json::from_str(&data).ok()) - .unwrap_or_else(|| HashSet::new()); + let mut cached_rank_list: HashSet = get_from_local_storage("ranks") + .and_then(|data| serde_json::from_str(&data).ok()) + .unwrap_or_else(|| HashSet::new()); + normalize_rank_filter_selection(&mut cached_rank_list); let rank_list = RwSignal::new(cached_rank_list); + let mut cached_core_rank_list: HashSet = get_from_local_storage("core_ranks") + .and_then(|data| serde_json::from_str(&data).ok()) + .unwrap_or_else(|| HashSet::new()); + normalize_rank_filter_selection(&mut cached_core_rank_list); + let core_rank_list = RwSignal::new(cached_core_rank_list); + let mut cached_thcpl_rank_list: HashSet = get_from_local_storage("thcpl_ranks") + .and_then(|data| serde_json::from_str(&data).ok()) + .unwrap_or_else(|| HashSet::new()); + normalize_rank_filter_selection(&mut cached_thcpl_rank_list); + let thcpl_rank_list = RwSignal::new(cached_thcpl_rank_list); + let open_dropdown = RwSignal::new(None::); // liked let cached_like_list: HashSet = get_from_local_storage("likes") - .and_then(|data| serde_json::from_str(&data).ok()) - .unwrap_or_else(|| HashSet::new()); + .and_then(|data| serde_json::from_str(&data).ok()) + .unwrap_or_else(|| HashSet::new()); let like_list = RwSignal::new(cached_like_list); let show_subscription_modal = RwSignal::new(false); @@ -93,6 +107,8 @@ pub fn ShowTable() -> impl IntoView { let _ = check_list.get(); let _ = input_value.get(); let _ = rank_list.get(); + let _ = core_rank_list.get(); + let _ = thcpl_rank_list.get(); if is_filter_change.get_untracked() { page.set(1); @@ -105,6 +121,14 @@ pub fn ShowTable() -> impl IntoView { set_in_local_storage("use_english", &use_english.get().to_string()); set_in_local_storage("types", &serde_json::to_string(&check_list.get()).unwrap()); set_in_local_storage("ranks", &serde_json::to_string(&rank_list.get()).unwrap()); + set_in_local_storage( + "core_ranks", + &serde_json::to_string(&core_rank_list.get()).unwrap(), + ); + set_in_local_storage( + "thcpl_ranks", + &serde_json::to_string(&thcpl_rank_list.get()).unwrap(), + ); }); Effect::new(move |_| { @@ -115,9 +139,11 @@ pub fn ShowTable() -> impl IntoView { let _ = check_list.get(); let _ = input_value.get(); let _ = rank_list.get(); + let _ = core_rank_list.get(); + let _ = thcpl_rank_list.get(); let _ = page.get(); - let (current_time, current_timezone) = get_browser_time_and_timezone(); + let (current_time, _) = get_browser_time_and_timezone(); let utc_map = load_utc_map(); all_conf_list.update(|conferences| { @@ -193,7 +219,8 @@ pub fn ShowTable() -> impl IntoView { let mut flag = false; let len = year_conf.timeline.len(); let mut cur_deadline = year_conf.timeline[len - 1].deadline.clone(); - let mut cur_abstract_deadline = year_conf.timeline[len - 1].abstract_deadline.clone(); + let mut cur_abstract_deadline = + year_conf.timeline[len - 1].abstract_deadline.clone(); let mut cur_comment = year_conf.timeline[len - 1].comment.clone(); let mut ddl_vec = Vec::::new(); @@ -224,13 +251,25 @@ pub fn ShowTable() -> impl IntoView { format!("{}T23:59:59{}", abs_ddl, tz_offset) }; - if let Ok(abs_ddl_datetime) = DateTime::parse_from_rfc3339(&abs_ddl_str) { - ddl_vec.push(TimePoint { timepoint: abs_ddl_datetime.with_timezone(¤t_timezone).clone(), r#type: 0 }); + if let Ok(abs_ddl_datetime) = + DateTime::parse_from_rfc3339(&abs_ddl_str) + { + ddl_vec.push(TimePoint { + timepoint: abs_ddl_datetime + .with_timezone(¤t_timezone) + .clone(), + r#type: 0, + }); } } if let Ok(ddl_datetime) = DateTime::parse_from_rfc3339(&ddl_str) { - ddl_vec.push(TimePoint { timepoint: ddl_datetime.with_timezone(¤t_timezone).clone(), r#type: 1 }); + ddl_vec.push(TimePoint { + timepoint: ddl_datetime + .with_timezone(¤t_timezone) + .clone(), + r#type: 1, + }); let diff = ddl_datetime.signed_duration_since(current_time); if !flag && diff.num_milliseconds() > 0 { @@ -274,7 +313,7 @@ pub fn ShowTable() -> impl IntoView { google_calendar_url: None, icloud_calendar_url: None, acc_str: None, - ddls: ddl_vec + ddls: ddl_vec, } }); conf_vec.extend(conf_items); @@ -369,14 +408,19 @@ pub fn ShowTable() -> impl IntoView { item.status = "RUN".to_string(); } - let iso_string = local_ddl_datetime.format("%Y%m%dT%H%M%S").to_string(); + let iso_string = + local_ddl_datetime.format("%Y%m%dT%H%M%S").to_string(); item.google_calendar_url = Some(format!( "https://www.google.com/calendar/render?action=TEMPLATE&text={}&dates={}/{}&details={:?}&location=Online&ctz={}&sf=true&output=xml", encode(&format!("{} {}", item.title, item.year)), iso_string, iso_string, - encode(&format!("{} {}", item.comment.as_ref().map_or("".to_string(), |c| c.clone()), "provided by @ccfddl".to_string())), + encode(&format!( + "{} {}", + item.comment.as_ref().map_or("".to_string(), |c| c.clone()), + "provided by @ccfddl".to_string() + )), time_zone.get_untracked(), )); @@ -416,7 +460,9 @@ pub fn ShowTable() -> impl IntoView { all_conf_list.update(|conferences| { for item in conferences.iter_mut() { for y in 1..=3 { - if item.title == acc_item.title && item.year == cur_acc.year + y { + if item.title == acc_item.title + && item.year == cur_acc.year + y + { item.acc_str = Some(cur_acc.str.clone()); } } @@ -430,8 +476,6 @@ pub fn ShowTable() -> impl IntoView { } } }); - - }); let paginated_list = Memo::new(move |_| { @@ -447,11 +491,28 @@ pub fn ShowTable() -> impl IntoView { if !rank_val.is_empty() { filtered_list.retain(|item| rank_val.contains(&item.rank)); } + let core_rank_val = core_rank_list.get(); + if !core_rank_val.is_empty() { + filtered_list.retain(|item| { + let core_rank = item.corerank.as_deref().unwrap_or("N"); + core_rank_val.contains(core_rank) + }); + } + let thcpl_rank_val = thcpl_rank_list.get(); + if !thcpl_rank_val.is_empty() { + filtered_list.retain(|item| { + let thcpl_rank = item.thcplrank.as_deref().unwrap_or("N"); + thcpl_rank_val.contains(thcpl_rank) + }); + } let input_val = input_value.get(); if !input_val.is_empty() { let input_lower = input_val.to_lowercase(); - filtered_list.retain(|item| item.id.to_lowercase().contains(&input_lower) || item.title.to_lowercase().contains(&input_lower)); + filtered_list.retain(|item| { + item.id.to_lowercase().contains(&input_lower) + || item.title.to_lowercase().contains(&input_lower) + }); } // Sorting and Grouping @@ -460,7 +521,7 @@ pub fn ShowTable() -> impl IntoView { .filter(|item| item.status == "RUN".to_string()) .cloned() .collect(); - let mut tbd_list: Vec<_> = filtered_list + let tbd_list: Vec<_> = filtered_list .iter() .filter(|item| item.status == "TBD".to_string()) .cloned() @@ -510,7 +571,6 @@ pub fn ShowTable() -> impl IntoView { } }); - view! {
@@ -569,24 +629,33 @@ pub fn ShowTable() -> impl IntoView {
-
-
- "Deadlines are shown in "{move || time_zone.get()}" time." -
-
- - - - - +
+
+
+ "Deadlines are shown in "{move || time_zone.get()}" time." +
+
+ + + + + +
-
+
- +
+ + + +
@@ -604,6 +703,8 @@ pub fn ShowTable() -> impl IntoView { use_english=use_english check_list=check_list rank_list=rank_list + core_rank_list=core_rank_list + thcpl_rank_list=thcpl_rank_list />
@@ -632,6 +733,26 @@ pub fn ShowTable() -> impl IntoView { children=move |conf| { let is_finished = conf.status == "FIN"; let is_tbd = conf.status == "TBD"; + let ccf_rank_value = conf.rank.clone(); + let ccf_rank_label = conf.displayrank.clone(); + let core_rank_value = conf + .corerank + .clone() + .unwrap_or_else(|| "N".to_string()); + let core_tag_label = if core_rank_value == "N" { + "Non-CORE".to_string() + } else { + format!("CORE {}", core_rank_value.clone()) + }; + let thcpl_rank_value = conf + .thcplrank + .clone() + .unwrap_or_else(|| "N".to_string()); + let thcpl_tag_label = if thcpl_rank_value == "N" { + "Non-THCPL".to_string() + } else { + format!("THCPL {}", thcpl_rank_value.clone()) + }; let show_ddl_str = if is_tbd { "TBD".to_string() } else { @@ -727,42 +848,42 @@ pub fn ShowTable() -> impl IntoView {
- - {move || { - if conf.displayrank == "N" { - "Non-CCF".to_string() - } else { - conf.displayrank.clone() - } - }} - + + + {ccf_rank_label.clone()} + + + " " + + + {core_tag_label.clone()} + + + " " + + + {thcpl_tag_label.clone()} + + " " - {move || { - conf.corerank - .as_ref() - .filter(|corerank| corerank.as_str() != "N") - .map(|corerank| { - view! { - - {format!("CORE {} ", corerank.clone())} - - " " - } - }) - }} - {move || { - conf.thcplrank - .as_ref() - .filter(|thcplrank| thcplrank.as_str() != "N") - .map(|thcplrank| { - view! { - - {format!("THCPL {} ", thcplrank.clone())} - - " " - } - }) - }} {move || { conf.comment .as_ref() @@ -906,6 +1027,17 @@ pub fn ShowTable() -> impl IntoView {
+ +
} } @@ -932,8 +1064,8 @@ fn load_utc_map() -> HashMap { #[cfg(target_arch = "wasm32")] fn get_browser_time_and_timezone() -> (DateTime, FixedOffset) { - let utc_now = Utc::now(); - let js_date = js_sys::Date::new_0(); + let utc_now = chrono::Utc::now(); + let js_date = web_sys::js_sys::Date::new_0(); let offset_minutes = -(js_date.get_timezone_offset() as i32); let timezone = FixedOffset::east_opt(offset_minutes * 60) @@ -969,15 +1101,36 @@ fn is_mobile_device() -> bool { let window = web_sys::window().expect("no global window exists"); let navigator = window.navigator(); - let user_agent = navigator.user_agent().expect("user agent not available").to_lowercase(); + let user_agent = navigator + .user_agent() + .expect("user agent not available") + .to_lowercase(); let mobile_keywords = [ - "phone", "pad", "pod", "iphone", "ipod", "ios", "ipad", "android", - "mobile", "blackberry", "iemobile", "mqqbrowser", "juc", "fennec", - "wosbrowser", "browserng", "webos", "symbian", "windows phone" + "phone", + "pad", + "pod", + "iphone", + "ipod", + "ios", + "ipad", + "android", + "mobile", + "blackberry", + "iemobile", + "mqqbrowser", + "juc", + "fennec", + "wosbrowser", + "browserng", + "webos", + "symbian", + "windows phone", ]; - mobile_keywords.iter().any(|&keyword| user_agent.contains(keyword)) + mobile_keywords + .iter() + .any(|&keyword| user_agent.contains(keyword)) } fn get_from_local_storage(key: &str) -> Option { diff --git a/src/components/subscription_modal.rs b/src/components/subscription_modal.rs index 1b2bcab6d..52bac3048 100644 --- a/src/components/subscription_modal.rs +++ b/src/components/subscription_modal.rs @@ -13,123 +13,178 @@ pub struct SubscriptionLink { pub type IcsSubscription = SubscriptionLink; -pub fn generate_ics_urls( - lang: &str, - subs: &HashSet, - ranks: &HashSet, -) -> Vec { - let base_url = "webcal://ccfddl.com/conference"; - let mut urls = Vec::new(); +fn sanitize_filter_value(value: &str) -> String { + value.replace('*', "star") +} - if subs.is_empty() && ranks.is_empty() { - urls.push(IcsSubscription { - url: format!("{}/deadlines_{}.ics", base_url, lang), - description: if lang == "zh" { - "所有会议".to_string() - } else { - "All Conferences".to_string() - }, - }); - return urls; +fn format_rank_label(system: &str, rank: &str) -> String { + match (system, rank) { + ("CCF", "N") => "Non-CCF".to_string(), + ("CORE", "N") => "Non-CORE".to_string(), + ("THCPL", "N") => "Non-THCPL".to_string(), + _ => format!("{} {}", system, rank), } +} - if !subs.is_empty() && !ranks.is_empty() { - for sub in subs.iter() { - for rank in ranks.iter() { - let rank_prefix = get_rank_prefix(rank); - urls.push(IcsSubscription { - url: format!( - "{}/deadlines_{}_{}_{}_{}.ics", - base_url, lang, rank_prefix, rank, sub - ), - description: format!("{} {} {}", sub, rank_prefix.to_uppercase(), rank), - }); - } - } - } else if !subs.is_empty() { - for sub in subs.iter() { - urls.push(IcsSubscription { - url: format!("{}/deadlines_{}_{}.ics", base_url, lang, sub), - description: sub.clone(), - }); - } - } else if !ranks.is_empty() { - for rank in ranks.iter() { - let rank_prefix = get_rank_prefix(rank); - urls.push(IcsSubscription { - url: format!( - "{}/deadlines_{}_{}_{}.ics", - base_url, lang, rank_prefix, rank - ), - description: format!("{} {}", rank_prefix.to_uppercase(), rank), - }); - } +fn format_rank_summary_value(system: &str, rank: &str) -> String { + match (system, rank) { + ("CCF", "N") => "Non-CCF".to_string(), + ("CORE", "N") => "Non-CORE".to_string(), + ("THCPL", "N") => "Non-THCPL".to_string(), + _ => rank.to_string(), } +} - urls +fn sorted_values(values: &HashSet) -> Vec { + let mut sorted: Vec<_> = values.iter().cloned().collect(); + sorted.sort(); + sorted } -pub fn generate_rss_urls( +fn build_subscription_urls( + base_url: &str, + extension: &str, lang: &str, subs: &HashSet, - ranks: &HashSet, + ccf_ranks: &HashSet, + core_ranks: &HashSet, + thcpl_ranks: &HashSet, ) -> Vec { - let base_url = "https://ccfddl.com/conference"; - let mut urls = Vec::new(); - - if subs.is_empty() && ranks.is_empty() { - urls.push(SubscriptionLink { - url: format!("{}/deadlines_{}.xml", base_url, lang), + if subs.is_empty() && ccf_ranks.is_empty() && core_ranks.is_empty() && thcpl_ranks.is_empty() { + return vec![SubscriptionLink { + url: format!("{}/deadlines_{}.{}", base_url, lang, extension), description: if lang == "zh" { "所有会议".to_string() } else { "All Conferences".to_string() }, - }); - return urls; + }]; } - if !subs.is_empty() && !ranks.is_empty() { - for sub in subs.iter() { - for rank in ranks.iter() { - let rank_prefix = get_rank_prefix(rank); - urls.push(SubscriptionLink { - url: format!( - "{}/deadlines_{}_{}_{}_{}.xml", - base_url, lang, rank_prefix, rank, sub - ), - description: format!("{} {} {}", sub, rank_prefix.to_uppercase(), rank), - }); + let sub_tokens: Vec> = if subs.is_empty() { + vec![None] + } else { + sorted_values(subs) + .into_iter() + .map(|sub| Some((sub.clone(), sub))) + .collect() + }; + let ccf_tokens: Vec> = if ccf_ranks.is_empty() { + vec![None] + } else { + sorted_values(ccf_ranks) + .into_iter() + .map(|rank| { + Some(( + format!("ccf_{}", sanitize_filter_value(&rank)), + format_rank_label("CCF", &rank), + )) + }) + .collect() + }; + let core_tokens: Vec> = if core_ranks.is_empty() { + vec![None] + } else { + sorted_values(core_ranks) + .into_iter() + .map(|rank| { + Some(( + format!("core_{}", sanitize_filter_value(&rank)), + format_rank_label("CORE", &rank), + )) + }) + .collect() + }; + let thcpl_tokens: Vec> = if thcpl_ranks.is_empty() { + vec![None] + } else { + sorted_values(thcpl_ranks) + .into_iter() + .map(|rank| { + Some(( + format!("thcpl_{}", sanitize_filter_value(&rank)), + format_rank_label("THCPL", &rank), + )) + }) + .collect() + }; + + let mut urls = Vec::new(); + + for ccf in &ccf_tokens { + for core in &core_tokens { + for thcpl in &thcpl_tokens { + for sub in &sub_tokens { + let mut filename_parts = vec![format!("deadlines_{}", lang)]; + let mut description_parts = Vec::new(); + + if let Some((token, label)) = ccf { + filename_parts.push(token.clone()); + description_parts.push(label.clone()); + } + if let Some((token, label)) = core { + filename_parts.push(token.clone()); + description_parts.push(label.clone()); + } + if let Some((token, label)) = thcpl { + filename_parts.push(token.clone()); + description_parts.push(label.clone()); + } + if let Some((token, label)) = sub { + filename_parts.push(token.clone()); + description_parts.push(label.clone()); + } + + if description_parts.is_empty() { + continue; + } + + urls.push(SubscriptionLink { + url: format!("{}/{}.{}", base_url, filename_parts.join("_"), extension), + description: description_parts.join(" | "), + }); + } } } - } else if !subs.is_empty() { - for sub in subs.iter() { - urls.push(SubscriptionLink { - url: format!("{}/deadlines_{}_{}.xml", base_url, lang, sub), - description: sub.clone(), - }); - } - } else if !ranks.is_empty() { - for rank in ranks.iter() { - let rank_prefix = get_rank_prefix(rank); - urls.push(SubscriptionLink { - url: format!( - "{}/deadlines_{}_{}_{}.xml", - base_url, lang, rank_prefix, rank - ), - description: format!("{} {}", rank_prefix.to_uppercase(), rank), - }); - } } urls } -fn get_rank_prefix(rank: &str) -> &'static str { - match rank { - "A" | "B" | "C" | "N" => "ccf", - _ => "ccf", - } +pub fn generate_ics_urls( + lang: &str, + subs: &HashSet, + ccf_ranks: &HashSet, + core_ranks: &HashSet, + thcpl_ranks: &HashSet, +) -> Vec { + build_subscription_urls( + "webcal://ccfddl.com/conference", + "ics", + lang, + subs, + ccf_ranks, + core_ranks, + thcpl_ranks, + ) +} + +pub fn generate_rss_urls( + lang: &str, + subs: &HashSet, + ccf_ranks: &HashSet, + core_ranks: &HashSet, + thcpl_ranks: &HashSet, +) -> Vec { + build_subscription_urls( + "https://ccfddl.com/conference", + "xml", + lang, + subs, + ccf_ranks, + core_ranks, + thcpl_ranks, + ) } fn copy_text_to_clipboard(text: &str) { @@ -190,30 +245,75 @@ fn get_platform_instruction(use_english: bool) -> String { fn render_filter_summary( subs: &HashSet, - ranks: &HashSet, + ccf_ranks: &HashSet, + core_ranks: &HashSet, + thcpl_ranks: &HashSet, use_english: bool, ) -> String { let mut parts = Vec::new(); if !subs.is_empty() { - let mut sorted: Vec<_> = subs.iter().cloned().collect(); - sorted.sort(); + let sorted = sorted_values(subs); let label = if use_english { "Categories" } else { "分类" }; parts.push(format!("{}: {}", label, sorted.join(", "))); } - if !ranks.is_empty() { - let mut sorted: Vec<_> = ranks.iter().cloned().collect(); - sorted.sort(); - let label = if use_english { "Ranks" } else { "等级" }; + if !ccf_ranks.is_empty() { + let sorted = sorted_values(ccf_ranks) + .into_iter() + .map(|rank| format_rank_summary_value("CCF", &rank)) + .collect::>(); + let label = if use_english { + "CCF Ranks" + } else { + "CCF 等级" + }; + parts.push(format!("{}: {}", label, sorted.join(", "))); + } + if !core_ranks.is_empty() { + let sorted = sorted_values(core_ranks) + .into_iter() + .map(|rank| format_rank_summary_value("CORE", &rank)) + .collect::>(); + let label = if use_english { + "CORE Ranks" + } else { + "CORE 等级" + }; + parts.push(format!("{}: {}", label, sorted.join(", "))); + } + if !thcpl_ranks.is_empty() { + let sorted = sorted_values(thcpl_ranks) + .into_iter() + .map(|rank| format_rank_summary_value("THCPL", &rank)) + .collect::>(); + let label = if use_english { + "THCPL Ranks" + } else { + "THCPL 等级" + }; parts.push(format!("{}: {}", label, sorted.join(", "))); } if parts.is_empty() { if use_english { - "All conferences (no filters)".to_string() + "All conferences".to_string() } else { - "所有会议(未筛选)".to_string() + "全部会议".to_string() } } else { - parts.join(" | ") + parts.join(" · ") + } +} + +fn render_link_limit_message(link_count: usize, use_english: bool) -> String { + if use_english { + format!( + "{} links generated. Narrow the filters or subscribe to all instead.", + link_count + ) + } else { + format!( + "已生成 {} 个链接。建议缩小筛选范围,或直接订阅全部。", + link_count + ) } } @@ -223,22 +323,29 @@ pub fn SubscriptionModal( use_english: RwSignal, check_list: RwSignal>, rank_list: RwSignal>, + core_rank_list: RwSignal>, + thcpl_rank_list: RwSignal>, ) -> impl IntoView { let subscriptions = Memo::new(move |_| { let lang = if use_english.get() { "en" } else { "zh" }; let subs = check_list.get(); let ranks = rank_list.get(); - generate_ics_urls(lang, &subs, &ranks) + let core_ranks = core_rank_list.get(); + let thcpl_ranks = thcpl_rank_list.get(); + generate_ics_urls(lang, &subs, &ranks, &core_ranks, &thcpl_ranks) }); let rss_subscriptions = Memo::new(move |_| { let lang = if use_english.get() { "en" } else { "zh" }; let subs = check_list.get(); let ranks = rank_list.get(); - generate_rss_urls(lang, &subs, &ranks) + let core_ranks = core_rank_list.get(); + let thcpl_ranks = thcpl_rank_list.get(); + generate_rss_urls(lang, &subs, &ranks, &core_ranks, &thcpl_ranks) }); - let platform_hint = get_platform_instruction(use_english.get_untracked()); + let has_multiple_subscriptions = Memo::new(move |_| subscriptions.get().len() > 1); + let platform_hint = Memo::new(move |_| get_platform_instruction(use_english.get())); view! { @@ -257,29 +364,68 @@ pub fn SubscriptionModal(
{move || { if use_english.get() { - "Subscribe based on your current filters:" + "These links are generated from the filters currently active on this page." } else { - "根据当前筛选条件订阅:" + "下面的链接会直接继承你当前页面的筛选条件。" } }}
- {move || { - let subs = check_list.get(); - let ranks = rank_list.get(); - let en = use_english.get(); - render_filter_summary(&subs, &ranks, en) - }} +
+ {move || { + if use_english.get() { + "This subscription will include" + } else { + "本次订阅将包含" + } + }} +
+
+ {move || { + let subs = check_list.get(); + let ranks = rank_list.get(); + let core_ranks = core_rank_list.get(); + let thcpl_ranks = thcpl_rank_list.get(); + let en = use_english.get(); + render_filter_summary( + &subs, + &ranks, + &core_ranks, + &thcpl_ranks, + en, + ) + }} +
+ +
+ {move || { + if use_english.get() { + "Multiple links are shown because the current filters expand into separate subscription combinations." + } else { + "当前筛选会拆分出多个订阅组合,因此这里会显示多个链接。" + } + }} +
+
{move || { if use_english.get() { - "Subscription Links:" + "Calendar Subscription" } else { - "订阅链接:" + "日历订阅" + } + }} +
+
+ {move || { + if use_english.get() { + "Use these webcal links in Apple Calendar, Outlook, Google Calendar, or any app that supports calendar subscriptions." + } else { + "将这些 webcal 链接粘贴到支持订阅日历的应用中,例如 Apple 日历、Outlook、Google 日历等。" } }}
@@ -287,17 +433,7 @@ pub fn SubscriptionModal( {move || { let subs = subscriptions.get(); if subs.len() > 10 { - let msg = if use_english.get() { - format!( - "Too many filter combinations ({} links). Consider reducing filters or subscribe to all.", - subs.len(), - ) - } else { - format!( - "筛选组合过多({} 个链接)。建议减少筛选条件或订阅全部。", - subs.len(), - ) - }; + let msg = render_link_limit_message(subs.len(), use_english.get()); view! {
{msg} @@ -347,33 +483,42 @@ pub fn SubscriptionModal( .into_any() } }} + +
+ {move || { + if use_english.get() { + "Copy one link, then paste it into your calendar app's \"Subscribe by URL\" entry." + } else { + "复制一个链接,然后粘贴到日历应用的“通过 URL 订阅”入口。" + } + }} +
{move || { if use_english.get() { - "RSS Feed:" + "RSS Feed" } else { "RSS 订阅:" } }}
+
+ {move || { + if use_english.get() { + "Use RSS if you prefer deadline updates in an RSS reader instead of a calendar app." + } else { + "如果你更习惯用 RSS 阅读器跟踪截止日期更新,可以使用下面的 RSS 链接。" + } + }} +
{move || { let subs = rss_subscriptions.get(); if subs.len() > 10 { - let msg = if use_english.get() { - format!( - "Too many filter combinations ({} links). Consider reducing filters or subscribe to all.", - subs.len(), - ) - } else { - format!( - "筛选组合过多({} 个链接)。建议减少筛选条件或订阅全部。", - subs.len(), - ) - }; + let msg = render_link_limit_message(subs.len(), use_english.get()); view! {
{msg} @@ -427,9 +572,9 @@ pub fn SubscriptionModal(
{move || { if use_english.get() { - "Paste into your RSS reader" + "Paste the copied link into your RSS reader." } else { - "粘贴到 RSS 阅读器中" + "将复制的链接粘贴到 RSS 阅读器中。" } }}
@@ -439,9 +584,9 @@ pub fn SubscriptionModal(
{move || { if use_english.get() { - "How to Subscribe:" + "Quick tip" } else { - "如何订阅:" + "使用提示" } }}
@@ -450,22 +595,16 @@ pub fn SubscriptionModal( if use_english.get() { view! {
-
"1. Click the Copy button next to the link"
-
"2. Open your calendar app (Google Calendar, Apple Calendar, Outlook, etc.)"
-
"3. Find Subscribe to Calendar or Add Calendar by URL"
-
"4. Paste the copied link and confirm"
-
"5. The calendar will automatically sync new deadlines"
+
"Copy one link and paste it into your calendar app's Subscribe by URL entry."
+
"If multiple links appear, each link matches a different filter combination."
} .into_any() } else { view! {
-
"1. 点击链接旁的 复制 按钮"
-
"2. 打开日历应用(Google 日历、Apple 日历、Outlook 等)"
-
"3. 找到 订阅日历 或 通过 URL 添加日历 选项"
-
"4. 粘贴复制的链接并确认"
-
"5. 日历将自动同步最新截止日期"
+
"复制一个链接,并粘贴到日历应用的“通过 URL 订阅”入口。"
+
"如果这里出现多个链接,表示每个链接对应一个不同的筛选组合。"
} .into_any() @@ -473,16 +612,16 @@ pub fn SubscriptionModal( }}
- {platform_hint.clone()} + {move || platform_hint.get()}
{move || { if use_english.get() { - "Calendars typically update every 12-24 hours" + "Subscribed calendars usually refresh every 12-24 hours." } else { - "日历通常每 12-24 小时更新一次" + "订阅的日历通常每 12-24 小时刷新一次。" } }}
diff --git a/src/components/timeline.rs b/src/components/timeline.rs index a8a34cb00..844e77b4f 100644 --- a/src/components/timeline.rs +++ b/src/components/timeline.rs @@ -1,8 +1,6 @@ -use leptos::*; -use leptos::prelude::*; -use chrono::{prelude::*, Duration}; use crate::components::conf::TimePoint; -use web_sys::js_sys; +use chrono::{Duration, prelude::*}; +use leptos::prelude::*; #[component] pub fn TimeLine(time_points: Vec) -> impl IntoView { @@ -36,21 +34,30 @@ pub fn TimeLine(time_points: Vec) -> impl IntoView { ); }); - let format_time_label = move |value: &DateTime, is_day: bool, index: usize| -> String { - if !is_day { - return format!("{}", value.format("%Y/%m/%d %H:%M:%S")); - } + let format_time_label = + move |value: &DateTime, is_day: bool, index: usize| -> String { + if !is_day { + return format!("{}", value.format("%Y/%m/%d %H:%M:%S")); + } - let tips = date_tips.get(); - if tips.len() > 1 && index < tips.len() - 1 { - let cur_percent = calculate_position_percent(&tips[index].timepoint, start_date.get(), end_date.get()); - let next_percent = calculate_position_percent(&tips[index + 1].timepoint, start_date.get(), end_date.get()); - if next_percent - cur_percent < 8.0 { - return String::new(); + let tips = date_tips.get(); + if tips.len() > 1 && index < tips.len() - 1 { + let cur_percent = calculate_position_percent( + &tips[index].timepoint, + start_date.get(), + end_date.get(), + ); + let next_percent = calculate_position_percent( + &tips[index + 1].timepoint, + start_date.get(), + end_date.get(), + ); + if next_percent - cur_percent < 8.0 { + return String::new(); + } } - } - format!("{}", value.format("%m/%d")) - }; + format!("{}", value.format("%m/%d")) + }; let format_backup_type = |backup_type: i32| -> &'static str { match backup_type { @@ -366,8 +373,8 @@ fn calculate_position_percent(time: &DateTime, start: f64, end: f64 #[cfg(target_arch = "wasm32")] fn get_browser_time_and_timezone() -> (DateTime, FixedOffset) { - let utc_now = Utc::now(); - let js_date = js_sys::Date::new_0(); + let utc_now = chrono::Utc::now(); + let js_date = web_sys::js_sys::Date::new_0(); let offset_minutes = -(js_date.get_timezone_offset() as i32); let timezone = FixedOffset::east_opt(offset_minutes * 60) @@ -414,7 +421,10 @@ fn initialize_timeline( let is_single_point = time_points.len() == 1; if is_single_point { - deadlines.push(TimePoint { timepoint: now, r#type: 1 }); + deadlines.push(TimePoint { + timepoint: now, + r#type: 1, + }); set_is_single.set(true); } else { set_is_single.set(false); @@ -449,7 +459,10 @@ fn initialize_timeline( let width = progress_ratio * 100.0; let left = (now_timestamp - start_time) / (end_time - start_time) * 100.0; let max_width = 100.0 - left; - set_can_line_style.set(format!("width:{}%;left:{}%;max-width:{}%;", width, left, max_width)); + set_can_line_style.set(format!( + "width:{}%;left:{}%;max-width:{}%;", + width, left, max_width + )); } else { set_can_line_style.set("width:0%;".to_string()); } @@ -458,8 +471,14 @@ fn initialize_timeline( set_date_tips.set(deadlines.clone()); let mut all_incremental = deadlines.clone(); - all_incremental.push(TimePoint { timepoint: deadlines.last().unwrap().timepoint, r#type: 1 }); - all_incremental.push(TimePoint { timepoint: now, r#type: 1 }); + all_incremental.push(TimePoint { + timepoint: deadlines.last().unwrap().timepoint, + r#type: 1, + }); + all_incremental.push(TimePoint { + timepoint: now, + r#type: 1, + }); set_all_incre.set(all_incremental); update_selected_dot( diff --git a/src/components/timezone.rs b/src/components/timezone.rs index 555eab506..fd645870c 100644 --- a/src/components/timezone.rs +++ b/src/components/timezone.rs @@ -23,8 +23,7 @@ pub fn get_timezone_name_or_utc() -> String { // Get timezone name and validate it's supported by chrono-tz #[allow(dead_code)] pub fn get_supported_timezone() -> Option { - get_timezone_name() - .and_then(|tz_name| Tz::from_str(&tz_name).ok()) + get_timezone_name().and_then(|tz_name| Tz::from_str(&tz_name).ok()) } // Get timezone name or return UTC timezone if not supported diff --git a/src/main.rs b/src/main.rs index 46b1b3e97..fdb612130 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use leptos::prelude::*; use ccfddl::App; +use leptos::prelude::*; fn main() { // set up logging diff --git a/src/pages/home.rs b/src/pages/home.rs index ae05a0954..5a971f2d0 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -1,22 +1,33 @@ -use leptos::prelude::*; use crate::components::header::Header; use crate::components::showtable::ShowTable; +use leptos::prelude::*; use thaw::*; - /// Default Home Page #[component] pub fn Home() -> impl IntoView { // theme let theme = RwSignal::new(Theme::light()); theme.update(|theme| { - theme.color.set_color_compound_brand_background("#409eff".to_string()); - theme.color.set_color_compound_brand_background_hover("#409eff".to_string()); - theme.color.set_color_neutral_stroke_accessible("#dcdfe6".to_string()); - theme.color.set_color_neutral_stroke_accessible_pressed("#409eff".to_string()); - theme.color.set_color_neutral_stroke_accessible_hover("#409eff".to_string()); - theme.color.set_color_neutral_stroke_2("#ebeef5".to_string()); + theme + .color + .set_color_compound_brand_background("#409eff".to_string()); + theme + .color + .set_color_compound_brand_background_hover("#409eff".to_string()); + theme + .color + .set_color_neutral_stroke_accessible("#dcdfe6".to_string()); + theme + .color + .set_color_neutral_stroke_accessible_pressed("#409eff".to_string()); + theme + .color + .set_color_neutral_stroke_accessible_hover("#409eff".to_string()); + theme + .color + .set_color_neutral_stroke_2("#ebeef5".to_string()); }); view! {