| … | … | @@ -23,6 +23,11 @@ custom_minimum_size = Vector2(48, 48) |
| 23 | 23 | text = "K" |
| 24 | 24 | tooltip_text = "Home — My Rooms" |
| 25 | 25 | |
| 26 | [node name="ViewSelect" type="OptionButton" parent="Margin/VBox"] | |
| 27 | unique_name_in_owner = true | |
| 28 | layout_mode = 2 | |
| 29 | tooltip_text = "Group view mode" | |
| 30 | ||
| 26 | 31 | [node name="HSeparator" type="HSeparator" parent="Margin/VBox"] |
| 27 | 32 | layout_mode = 2 |
| 28 | 33 | theme_override_constants/separation = 6 |
| … | … | @@ -19,6 +19,11 @@ size_flags_horizontal = 3 |
| 19 | 19 | text = "Rooms" |
| 20 | 20 | horizontal_alignment = 1 |
| 21 | 21 | |
| 22 | [node name="SortBtn" type="OptionButton" parent="VBox/TitleHBox"] | |
| 23 | unique_name_in_owner = true | |
| 24 | layout_mode = 2 | |
| 25 | tooltip_text = "Sort rooms" | |
| 26 | ||
| 22 | 27 | [node name="GroupSettingsBtn" type="Button" parent="VBox/TitleHBox"] |
| 23 | 28 | unique_name_in_owner = true |
| 24 | 29 | visible = false |
| … | … | @@ -7,6 +7,7 @@ signal group_selected(group_id: String) |
| 7 | 7 | @onready var group_icons: VBoxContainer = %GroupIcons |
| 8 | 8 | @onready var home_btn: Button = %HomeButton |
| 9 | 9 | @onready var add_btn: Button = %AddButton |
| 10 | @onready var view_select: OptionButton = %ViewSelect | |
| 10 | 11 | |
| 11 | 12 | var _registries: Array = [] |
| 12 | 13 | var _registry_meta: Dictionary = {} # reg_key -> meta dict |
| … | … | @@ -18,6 +19,16 @@ var _popup: PopupMenu = null |
| 18 | 19 | var _popup_reg_key: String = "" |
| 19 | 20 | var _settings_window: Window = null |
| 20 | 21 | var _avatar_picker: AvatarPicker = null |
| 22 | var _current_view: int = 0 # index into VIEW_MODES | |
| 23 | ||
| 24 | ## View modes: what drives the cluster icons | |
| 25 | const VIEW_MODES: Array[Dictionary] = [ | |
| 26 | {"label": "Groups", "algo": {}}, | |
| 27 | {"label": "Members", "algo": {"name": "members-only", "features": [{"type": "members", "weight": 1.0}], "threshold": 0.5, "minSize": 2}}, | |
| 28 | {"label": "Topics", "algo": {"name": "topics-only", "features": [{"type": "topics", "weight": 1.0}], "threshold": 0.3, "minSize": 2}}, | |
| 29 | {"label": "Category", "algo": {"name": "category-only", "features": [{"type": "category", "weight": 1.0}], "threshold": 0.1, "minSize": 2}}, | |
| 30 | {"label": "Activity", "algo": {"name": "activity-only", "features": [{"type": "activity", "weight": 1.0}], "threshold": 0.1, "minSize": 2}}, | |
| 31 | ] | |
| 21 | 32 | |
| 22 | 33 | const ICON_SIZE := 48 |
| 23 | 34 | const PILL_WIDTH := 4 |
| … | … | @@ -38,13 +49,36 @@ func _ready() -> void: |
| 38 | 49 | home_btn.pressed.connect(_on_home_pressed) |
| 39 | 50 | add_btn.pressed.connect(_on_add_pressed) |
| 40 | 51 | |
| 52 | # Populate view mode selector | |
| 53 | for i: int in VIEW_MODES.size(): | |
| 54 | view_select.add_item(VIEW_MODES[i]["label"], i) | |
| 55 | view_select.selected = 0 | |
| 56 | view_select.item_selected.connect(_on_view_mode_changed) | |
| 57 | ||
| 41 | 58 | if KeetService.client.is_connected: |
| 42 | 59 | _on_connected() |
| 43 | 60 | |
| 44 | 61 | |
| 45 | 62 | func _on_connected() -> void: |
| 46 | 63 | KeetService.discovery.list_registries() |
| 47 | KeetService.graph_mgr.compute() | |
| 64 | _recompute_clusters() | |
| 65 | ||
| 66 | ||
| 67 | func _on_view_mode_changed(idx: int) -> void: | |
| 68 | _current_view = idx | |
| 69 | _clusters.clear() | |
| 70 | _rebuild_icons() | |
| 71 | _recompute_clusters() | |
| 72 | ||
| 73 | ||
| 74 | func _recompute_clusters() -> void: | |
| 75 | var algo: Dictionary = VIEW_MODES[_current_view]["algo"] | |
| 76 | if algo.is_empty(): | |
| 77 | # "Groups" mode — no clusters, just registries | |
| 78 | _clusters.clear() | |
| 79 | _rebuild_icons() | |
| 80 | else: | |
| 81 | KeetService.graph_mgr.compute(true, algo) | |
| 48 | 82 | |
| 49 | 83 | |
| 50 | 84 | func _on_home_pressed() -> void: |
| … | … | @@ -251,25 +285,21 @@ func _rebuild_icons() -> void: |
| 251 | 285 | var icon := _create_group_icon(room_id, label, desc, avatar_b64, is_selected) |
| 252 | 286 | group_icons.add_child(icon) |
| 253 | 287 | |
| 254 | # Cluster icons (MI-computed groups, skip trivial ones) | |
| 255 | var registry_rooms: Dictionary = {} | |
| 256 | for reg: Variant in _registries: | |
| 257 | if reg is Dictionary: | |
| 258 | registry_rooms[reg.get("roomId", "")] = true | |
| 259 | ||
| 260 | for cluster: Variant in _clusters: | |
| 261 | if not cluster is Dictionary: | |
| 262 | continue | |
| 263 | var rooms: Array = cluster.get("rooms", []) | |
| 264 | if rooms.size() < 2: | |
| 265 | continue | |
| 266 | var cluster_id: String = "cluster:%d" % cluster.get("id", 0) | |
| 267 | var label: String = cluster.get("label", "") | |
| 268 | if label == "": | |
| 269 | label = "%d rooms" % rooms.size() | |
| 270 | var is_selected: bool = cluster_id == _active_group | |
| 271 | var c_icon := _create_group_icon(cluster_id, label, "Computed cluster", "", is_selected) | |
| 272 | group_icons.add_child(c_icon) | |
| 288 | # Cluster icons — only shown when a clustering algo is selected | |
| 289 | if _current_view > 0: | |
| 290 | for cluster: Variant in _clusters: | |
| 291 | if not cluster is Dictionary: | |
| 292 | continue | |
| 293 | var rooms: Array = cluster.get("rooms", []) | |
| 294 | if rooms.size() < 1: | |
| 295 | continue | |
| 296 | var cluster_id: String = "cluster:%d" % cluster.get("id", 0) | |
| 297 | var label: String = cluster.get("label", "") | |
| 298 | if label == "": | |
| 299 | label = "%d rooms" % rooms.size() | |
| 300 | var is_selected: bool = cluster_id == _active_group | |
| 301 | var c_icon := _create_group_icon(cluster_id, label, VIEW_MODES[_current_view]["label"], "", is_selected) | |
| 302 | group_icons.add_child(c_icon) | |
| 273 | 303 | |
| 274 | 304 | |
| 275 | 305 | func _style_home_button() -> void: |
| … | … | @@ -8,6 +8,7 @@ extends PanelContainer |
| 8 | 8 | enum ViewMode { HOME, GROUP_JOINED, GROUP_DISCOVER } |
| 9 | 9 | |
| 10 | 10 | @onready var title_label: Label = %Title |
| 11 | @onready var sort_btn: OptionButton = %SortBtn | |
| 11 | 12 | @onready var group_settings_btn: Button = %GroupSettingsBtn |
| 12 | 13 | @onready var search_field: LineEdit = %SearchField |
| 13 | 14 | @onready var my_rooms_list: VBoxContainer = %MyRoomsList |
| … | … | @@ -53,6 +54,10 @@ var _topic_searching: bool = false |
| 53 | 54 | ## Cluster metadata from graph (cluster_id_str -> {label, topics}) |
| 54 | 55 | var _cluster_meta_cache: Dictionary = {} |
| 55 | 56 | |
| 57 | ## Sort mode | |
| 58 | enum SortMode { DEFAULT, NAME_AZ, NAME_ZA } | |
| 59 | var _sort_mode: SortMode = SortMode.DEFAULT | |
| 60 | ||
| 56 | 61 | ## Style constants |
| 57 | 62 | const COLOR_SELECTED := Color(0.25, 0.35, 0.55) |
| 58 | 63 | const COLOR_HOVER := Color(0.2, 0.25, 0.35) |
| … | … | @@ -86,6 +91,13 @@ func _ready() -> void: |
| 86 | 91 | group_settings_btn.pressed.connect(_on_group_settings_pressed) |
| 87 | 92 | KeetService.discovery.topic_results.connect(_on_topic_results) |
| 88 | 93 | |
| 94 | # Sort button | |
| 95 | sort_btn.add_item("Default", 0) | |
| 96 | sort_btn.add_item("Name A-Z", 1) | |
| 97 | sort_btn.add_item("Name Z-A", 2) | |
| 98 | sort_btn.selected = 0 | |
| 99 | sort_btn.item_selected.connect(_on_sort_changed) | |
| 100 | ||
| 89 | 101 | # Make title clickable for discover mode toggle |
| 90 | 102 | title_label.mouse_filter = Control.MOUSE_FILTER_STOP |
| 91 | 103 | title_label.gui_input.connect(_on_title_gui_input) |
| … | … | @@ -234,6 +246,35 @@ func _render_group_discover_view() -> void: |
| 234 | 246 | _rebuild_current_group_view() |
| 235 | 247 | |
| 236 | 248 | |
| 249 | func _on_sort_changed(idx: int) -> void: | |
| 250 | match idx: | |
| 251 | 0: _sort_mode = SortMode.DEFAULT | |
| 252 | 1: _sort_mode = SortMode.NAME_AZ | |
| 253 | 2: _sort_mode = SortMode.NAME_ZA | |
| 254 | match _view_mode: | |
| 255 | ViewMode.HOME: | |
| 256 | _refresh_my_rooms() | |
| 257 | ViewMode.GROUP_JOINED: | |
| 258 | _build_joined_group_rooms() | |
| 259 | ViewMode.GROUP_DISCOVER: | |
| 260 | _build_discover_group_rooms() | |
| 261 | ||
| 262 | ||
| 263 | func _sorted_rooms(rooms: Array) -> Array: | |
| 264 | if _sort_mode == SortMode.DEFAULT: | |
| 265 | return rooms | |
| 266 | var sorted: Array = rooms.duplicate() | |
| 267 | sorted.sort_custom(func(a: Variant, b: Variant) -> bool: | |
| 268 | var ta: String = a.get("title", "") if a is Dictionary else "" | |
| 269 | var tb: String = b.get("title", "") if b is Dictionary else "" | |
| 270 | if _sort_mode == SortMode.NAME_AZ: | |
| 271 | return ta.to_lower() < tb.to_lower() | |
| 272 | else: | |
| 273 | return ta.to_lower() > tb.to_lower() | |
| 274 | ) | |
| 275 | return sorted | |
| 276 | ||
| 277 | ||
| 237 | 278 | func _on_title_gui_input(event: InputEvent) -> void: |
| 238 | 279 | if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: |
| 239 | 280 | if _view_mode == ViewMode.GROUP_JOINED: |
| … | … | @@ -333,7 +374,7 @@ func _build_joined_group_rooms() -> void: |
| 333 | 374 | |
| 334 | 375 | # Show only MY rooms that are also in the group's discovery |
| 335 | 376 | var shown: int = 0 |
| 336 | for room: Variant in _rooms: | |
| 377 | for room: Variant in _sorted_rooms(_rooms): | |
| 337 | 378 | if not room is Dictionary: |
| 338 | 379 | continue |
| 339 | 380 | var rid: String = room.get("roomId", "") |
| … | … | @@ -395,7 +436,7 @@ func _build_discover_group_rooms() -> void: |
| 395 | 436 | group_rooms_list.add_child(empty_label) |
| 396 | 437 | return |
| 397 | 438 | |
| 398 | for room: Variant in _group_rooms: | |
| 439 | for room: Variant in _sorted_rooms(_group_rooms): | |
| 399 | 440 | if not room is Dictionary: |
| 400 | 441 | continue |
| 401 | 442 | var title: String = room.get("title", "(untitled)") |
| … | … | @@ -489,7 +530,7 @@ func _refresh_my_rooms() -> void: |
| 489 | 530 | my_rooms_list.add_child(empty_label) |
| 490 | 531 | return |
| 491 | 532 | |
| 492 | for room: Variant in _rooms: | |
| 533 | for room: Variant in _sorted_rooms(_rooms): | |
| 493 | 534 | if not room is Dictionary: |
| 494 | 535 | continue |
| 495 | 536 | var title: String = room.get("title", "(untitled)") |