better-rtplot 0.6.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/PKG-INFO +3 -1
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/README.md +2 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/pyproject.toml +1 -1
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/client.py +31 -2
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/server_browser.py +18 -5
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/index.html +25 -2
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/presentation.css +5 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/ui_state.py +26 -1
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/LICENSE +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/server.py +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/uPlot.min.css +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/ui-view.css +0 -0
- {better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/ui-view.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better-rtplot
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary:
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -159,3 +159,5 @@ A rising line now draws itself in the browser tab.
|
|
|
159
159
|
Issues and feature requests:
|
|
160
160
|
[github.com/jmontp/rtplot/issues](https://github.com/jmontp/rtplot/issues).
|
|
161
161
|
|
|
162
|
+
Dropdown controls: [API and examples](docs/dropdowns.md).
|
|
163
|
+
|
|
@@ -310,10 +310,24 @@ class TextInput:
|
|
|
310
310
|
})
|
|
311
311
|
|
|
312
312
|
|
|
313
|
+
@dataclass
|
|
314
|
+
class Dropdown:
|
|
315
|
+
"""Single choice control. Omitted value selects the first option."""
|
|
316
|
+
id: str
|
|
317
|
+
label: str
|
|
318
|
+
options: List[Union[str, dict]]
|
|
319
|
+
value: Optional[str] = None
|
|
320
|
+
height: Optional[float] = None
|
|
321
|
+
|
|
322
|
+
def to_dict(self):
|
|
323
|
+
return _drop_none({"type": "dropdown", "id": self.id, "label": self.label,
|
|
324
|
+
"options": self.options, "value": self.value, "height": self.height})
|
|
325
|
+
|
|
326
|
+
|
|
313
327
|
@dataclass
|
|
314
328
|
class ControlsRow:
|
|
315
329
|
"""A row of control widgets, rendered in place of a plot."""
|
|
316
|
-
controls: List[Union[Button, Slider, Dial, Display, Text, TextInput, dict]] = field(default_factory=list)
|
|
330
|
+
controls: List[Union[Button, Slider, Dial, Display, Text, TextInput, Dropdown, dict]] = field(default_factory=list)
|
|
317
331
|
|
|
318
332
|
section: Optional[str] = None
|
|
319
333
|
title: Optional[str] = None
|
|
@@ -610,6 +624,10 @@ def initialize_plots(plot_descriptions=1, handshake_timeout=2.0, *, view=None, u
|
|
|
610
624
|
"confirm support: layout may be flat and disabled states are NOT enforced.",
|
|
611
625
|
RuntimeWarning, stacklevel=2)
|
|
612
626
|
|
|
627
|
+
if any(c["type"] == "dropdown" for c in _control_registry.values()) and "dropdown_v1" not in _server_capabilities:
|
|
628
|
+
warnings.warn("Dropdowns require rtplot browser server >= 0.6.1. This server did not "
|
|
629
|
+
"confirm support; dropdown controls may be missing or unenforced.",
|
|
630
|
+
RuntimeWarning, stacklevel=2)
|
|
613
631
|
if presentation_requested(plot_desc_dict, _view) and "presentation_v1" not in _server_capabilities:
|
|
614
632
|
warnings.warn("Presentation options require rtplot browser server >= 0.6.0. This server did not "
|
|
615
633
|
"confirm support; essential placement, groups, and styling may be ignored.",
|
|
@@ -767,12 +785,23 @@ def set_text_input(input_id: str, value):
|
|
|
767
785
|
socket.send_json({"id": str(input_id), "value": str(value)})
|
|
768
786
|
|
|
769
787
|
|
|
788
|
+
def set_dropdown(control_id: str, value: str):
|
|
789
|
+
"""Select a declared option without rebuilding the layout (server >= 0.6.1)."""
|
|
790
|
+
control = _control_registry.get(control_id, {})
|
|
791
|
+
if control.get("type") != "dropdown" or value not in control["options"]:
|
|
792
|
+
raise ValueError("set_dropdown requires a dropdown ID and a declared string value")
|
|
793
|
+
if "dropdown_v1" not in _server_capabilities:
|
|
794
|
+
raise RuntimeError("set_dropdown requires rtplot browser server >= 0.6.1")
|
|
795
|
+
socket.send_string(SENDING_TEXT_INPUT, zmq.SNDMORE)
|
|
796
|
+
socket.send_json({"id": control_id, "value": value, "session": _session, "generation": _generation})
|
|
797
|
+
|
|
798
|
+
|
|
770
799
|
def poll_controls():
|
|
771
800
|
"""Drain the return channel non-blocking and return current control state.
|
|
772
801
|
|
|
773
802
|
Returns a ControlState(values, buttons) where:
|
|
774
803
|
- values: dict of latest control values keyed by id. Sliders/dials
|
|
775
|
-
remain floats; text_input controls remain strings.
|
|
804
|
+
remain floats; text_input and dropdown controls remain strings.
|
|
776
805
|
- buttons: list of button ids that fired since the previous poll
|
|
777
806
|
(cleared after this call).
|
|
778
807
|
|
|
@@ -676,8 +676,9 @@ def parse_config(tab: Tab, json_config):
|
|
|
676
676
|
for element in row:
|
|
677
677
|
if element.get("type") in ("slider", "dial") and "value" in element:
|
|
678
678
|
slider_values[element["id"]] = float(element["value"])
|
|
679
|
-
elif element.get("type")
|
|
680
|
-
|
|
679
|
+
elif element.get("type") in ("text_input", "dropdown"):
|
|
680
|
+
default = registry[element["id"]]["options"][0] if element["type"] == "dropdown" else ""
|
|
681
|
+
text_values[element["id"]] = str(element.get("value", default))
|
|
681
682
|
|
|
682
683
|
for plot_counter, (_, plot_description) in enumerate(plot_entries):
|
|
683
684
|
trace_names = plot_description["names"]
|
|
@@ -717,10 +718,11 @@ def parse_config(tab: Tab, json_config):
|
|
|
717
718
|
el["id"]
|
|
718
719
|
for row in control_rows
|
|
719
720
|
for el in row
|
|
720
|
-
if el.get("type")
|
|
721
|
+
if el.get("type") in ("text_input", "dropdown")
|
|
721
722
|
}
|
|
722
723
|
tab.text_values = {
|
|
723
|
-
text_id:
|
|
724
|
+
text_id: (default_value if registry[text_id]["type"] == "dropdown"
|
|
725
|
+
else tab.text_values.get(text_id, default_value))
|
|
724
726
|
for text_id, default_value in text_values.items()
|
|
725
727
|
if text_id in active_text_ids
|
|
726
728
|
}
|
|
@@ -1264,6 +1266,12 @@ async def zmq_receiver(tab: Tab):
|
|
|
1264
1266
|
if text_id is None:
|
|
1265
1267
|
continue
|
|
1266
1268
|
value = str(payload.get("value", ""))
|
|
1269
|
+
control = tab.control_registry.get(text_id, {})
|
|
1270
|
+
if control.get("type") == "dropdown":
|
|
1271
|
+
if (payload.get("session"), payload.get("generation")) != (tab.session, tab.generation):
|
|
1272
|
+
continue
|
|
1273
|
+
if payload.get("value") not in control["options"]:
|
|
1274
|
+
continue
|
|
1267
1275
|
if tab.text_values.get(text_id) != value:
|
|
1268
1276
|
tab.text_values[text_id] = value
|
|
1269
1277
|
tab.text_dirty.add(text_id)
|
|
@@ -1701,8 +1709,13 @@ async def handle_ws(request):
|
|
|
1701
1709
|
text_id = payload.get("id")
|
|
1702
1710
|
tid = ws_tab.get(ws, BIND_ME_ID)
|
|
1703
1711
|
t = tabs.get(tid)
|
|
1704
|
-
if text_id and t is not None and control_allowed(t, payload, {"text_input"}):
|
|
1712
|
+
if text_id and t is not None and control_allowed(t, payload, {"text_input", "dropdown"}):
|
|
1705
1713
|
value = str(payload.get("value", ""))
|
|
1714
|
+
control = t.control_registry.get(text_id, {})
|
|
1715
|
+
if control.get("type") == "dropdown":
|
|
1716
|
+
if payload.get("value") not in control["options"]:
|
|
1717
|
+
continue
|
|
1718
|
+
t.text_dirty.add(text_id)
|
|
1706
1719
|
t.text_values[text_id] = value
|
|
1707
1720
|
refresh_config_message(t)
|
|
1708
1721
|
await send_control_event(
|
|
@@ -742,6 +742,28 @@
|
|
|
742
742
|
item.appendChild(val);
|
|
743
743
|
controlElements.displays[el.id] = val;
|
|
744
744
|
controlElements.displayKinds[el.id] = 'text';
|
|
745
|
+
} else if (el.type === 'dropdown') {
|
|
746
|
+
item.classList.add('ctrl-dropdown', 'flex');
|
|
747
|
+
const label = document.createElement('label');
|
|
748
|
+
label.textContent = el.label || el.id;
|
|
749
|
+
const select = document.createElement('select');
|
|
750
|
+
select.className = 'ctrl-textinput ctrl-select';
|
|
751
|
+
for (const choice of el.options) {
|
|
752
|
+
const option = document.createElement('option');
|
|
753
|
+
option.value = typeof choice === 'string' ? choice : choice.value;
|
|
754
|
+
option.textContent = typeof choice === 'string' ? choice : (choice.label ?? choice.value);
|
|
755
|
+
select.appendChild(option);
|
|
756
|
+
}
|
|
757
|
+
select.value = el.value ?? select.options[0].value;
|
|
758
|
+
let committed = select.value;
|
|
759
|
+
select.addEventListener('change', () => {
|
|
760
|
+
if (sendCtrl({type: 'control_text', id: el.id, value: select.value})) committed = select.value;
|
|
761
|
+
else select.value = committed;
|
|
762
|
+
});
|
|
763
|
+
select.setServerValue = value => { select.value = value; committed = value; };
|
|
764
|
+
label.appendChild(select);
|
|
765
|
+
item.appendChild(label);
|
|
766
|
+
controlElements.textInputs[el.id] = select;
|
|
745
767
|
} else if (el.type === 'text_input') {
|
|
746
768
|
item.classList.add('ctrl-text-input', 'flex');
|
|
747
769
|
if (el.label) {
|
|
@@ -809,7 +831,7 @@
|
|
|
809
831
|
applyElementSize(item, el);
|
|
810
832
|
item.dataset.appearance = el.appearance || 'default';
|
|
811
833
|
if (el.appearance === 'state') item.setAttribute('role', 'status');
|
|
812
|
-
item.querySelectorAll('input,svg.ctrl-dial').forEach(input => input.setAttribute('aria-label', el.label || el.id));
|
|
834
|
+
item.querySelectorAll('input,select,svg.ctrl-dial').forEach(input => input.setAttribute('aria-label', el.label || el.id));
|
|
813
835
|
div.appendChild(item);
|
|
814
836
|
viewManager.register(el.id, item, section);
|
|
815
837
|
});
|
|
@@ -828,7 +850,8 @@
|
|
|
828
850
|
if (!values) return;
|
|
829
851
|
for (const [id, val] of Object.entries(values)) {
|
|
830
852
|
const input = controlElements.textInputs[id];
|
|
831
|
-
if (input) input.
|
|
853
|
+
if (input?.setServerValue) input.setServerValue(String(val));
|
|
854
|
+
else if (input) input.value = (val !== undefined && val !== null) ? String(val) : '';
|
|
832
855
|
}
|
|
833
856
|
}
|
|
834
857
|
|
|
@@ -166,3 +166,8 @@ body { height: auto; min-height: 100%; }
|
|
|
166
166
|
.ctrl-textinput, .ctrl-numinput, .ctrl-nudgebtn { border-color: #738799; }
|
|
167
167
|
|
|
168
168
|
@media (max-height: 480px) and (min-width: 640px) { .ui-source-status { padding-block: 2px; font-size: 12px; line-height: 1.2; } }
|
|
169
|
+
|
|
170
|
+
/* Native choices retain platform keyboard and touch behavior. */
|
|
171
|
+
.ctrl-dropdown { min-width: 0; flex: 1 1 180px; }
|
|
172
|
+
.ctrl-dropdown > label { display: flex; flex-direction: column; gap: 6px; width: 100%; min-width: 0; }
|
|
173
|
+
.ctrl-select { flex: none; min-height: 44px; padding: 8px 12px; font: inherit; font-size: 16px; color: #18334b; background: #fff; border: 1px solid #738799; }
|
|
@@ -2,12 +2,35 @@
|
|
|
2
2
|
from copy import deepcopy
|
|
3
3
|
|
|
4
4
|
META_KEY = "__rtplot_view__"
|
|
5
|
-
CAPABILITIES = ["sections", "ui_state_v1", "presentation_v1"]
|
|
5
|
+
CAPABILITIES = ["sections", "ui_state_v1", "presentation_v1", "dropdown_v1"]
|
|
6
6
|
CONTROL_DEFAULTS = {"enabled": True, "visible": True, "busy": False, "selected": False, "reason": ""}
|
|
7
7
|
SECTION_DEFAULTS = {"visible": True, "status": "idle", "message": ""}
|
|
8
8
|
STATUSES = {"idle", "ready", "busy", "complete", "warning", "error"}
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def dropdown_options(control):
|
|
12
|
+
"""Return validated (value, label) choices; values are always strings."""
|
|
13
|
+
options = control.get("options")
|
|
14
|
+
if not isinstance(options, list) or not options:
|
|
15
|
+
raise ValueError("Dropdown options must be a non-empty list")
|
|
16
|
+
choices = []
|
|
17
|
+
for option in options:
|
|
18
|
+
if isinstance(option, str):
|
|
19
|
+
value, label = option, option
|
|
20
|
+
elif isinstance(option, dict):
|
|
21
|
+
value, label = option.get("value"), option.get("label", option.get("value"))
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError("Dropdown options must be strings or value/label dictionaries")
|
|
24
|
+
if not isinstance(value, str) or not isinstance(label, str):
|
|
25
|
+
raise ValueError("Dropdown option values and labels must be strings")
|
|
26
|
+
if value in [v for v, _ in choices]:
|
|
27
|
+
raise ValueError("Dropdown option values must be unique")
|
|
28
|
+
choices.append((value, label))
|
|
29
|
+
if control.get("value", choices[0][0]) not in [v for v, _ in choices]:
|
|
30
|
+
raise ValueError("Dropdown value must match an option")
|
|
31
|
+
return choices
|
|
32
|
+
|
|
33
|
+
|
|
11
34
|
def declarations(config, view=None):
|
|
12
35
|
"""Validate optional view and return normalized view plus control registry."""
|
|
13
36
|
if hasattr(view, "to_dict"):
|
|
@@ -78,6 +101,8 @@ def declarations(config, view=None):
|
|
|
78
101
|
if row.get("exclusive") and control.get("type") != "button":
|
|
79
102
|
raise ValueError("Exclusive groups may contain only buttons")
|
|
80
103
|
controls[cid] = {"type": control.get("type"), "section": sid}
|
|
104
|
+
if control.get("type") == "dropdown":
|
|
105
|
+
controls[cid]["options"] = [value for value, _ in dropdown_options(control)]
|
|
81
106
|
if row.get("exclusive"):
|
|
82
107
|
controls[cid]["exclusive_group"] = key
|
|
83
108
|
essentials = view.get("essential_controls", [])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.6.0 → better_rtplot-0.6.1}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|