Fold the sweep interfaces away by default

9e5a6e6 · Gabriel Morell · 2026-09-17 00:28

20 files +72 -36
Message
{commit_body(@commit)}

Files changed

modified android/app/src/main/java/org/neiam/lambent/app/ui/DevicesScreen.kt
+36 −12
@@ -1,5 +1,6 @@
1 1 package org.neiam.lambent.app.ui
2 2
3 +import androidx.compose.foundation.clickable
3 4 import androidx.compose.foundation.layout.Arrangement
4 5 import androidx.compose.foundation.layout.ExperimentalLayoutApi
5 6 import androidx.compose.foundation.layout.FlowRow
@@ -13,6 +14,8 @@ import androidx.compose.foundation.lazy.LazyColumn
13 14 import androidx.compose.foundation.lazy.items
14 15 import androidx.compose.material.icons.Icons
15 16 import androidx.compose.material.icons.filled.Bolt
17 +import androidx.compose.material.icons.filled.ExpandLess
18 +import androidx.compose.material.icons.filled.ExpandMore
16 19 import androidx.compose.material.icons.filled.Search
17 20 import androidx.compose.material.icons.filled.WbIridescent
18 21 import androidx.compose.material3.AssistChip
@@ -94,19 +97,40 @@ fun DevicesScreen() {
94 97 @OptIn(ExperimentalLayoutApi::class)
95 98 @Composable
96 99 private fun SweepInterfaces(interfaces: List<Interface>) {
100 + // folded away: it is set once and then forgotten about
101 + var open by remember { mutableStateOf(false) }
102 + val on = interfaces.count { it.enabled }
103 +
97 104 Column {
98 − Text("Sweep", style = MaterialTheme.typography.labelMedium)
99 − FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
100 − interfaces.forEach { iface ->
101 − Row(verticalAlignment = Alignment.CenterVertically) {
102 − Checkbox(
103 − checked = iface.enabled,
104 − onCheckedChange = { LambentSocket.sweepInterface(iface.name, it) },
105 − )
106 − Text(
107 − listOfNotNull(iface.name, iface.addr).joinToString(" "),
108 − style = MaterialTheme.typography.labelSmall,
109 − )
105 + Row(
106 + modifier = Modifier
107 + .fillMaxWidth()
108 + .clickable { open = !open },
109 + verticalAlignment = Alignment.CenterVertically,
110 + ) {
111 + Icon(
112 + if (open) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
113 + contentDescription = if (open) "Hide interfaces" else "Show interfaces",
114 + )
115 + Text(
116 + "Sweep · $on of ${interfaces.size} interfaces",
117 + style = MaterialTheme.typography.labelMedium,
118 + )
119 + }
120 +
121 + if (open) {
122 + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
123 + interfaces.forEach { iface ->
124 + Row(verticalAlignment = Alignment.CenterVertically) {
125 + Checkbox(
126 + checked = iface.enabled,
127 + onCheckedChange = { LambentSocket.sweepInterface(iface.name, it) },
128 + )
129 + Text(
130 + listOfNotNull(iface.name, iface.addr).joinToString(" "),
131 + style = MaterialTheme.typography.labelSmall,
132 + )
133 + }
110 134 }
111 135 }
112 136 }
modified lib/lambent_ex_web/live/devices/index.html.heex
+33 −24
@@ -19,30 +19,39 @@
19 19 <i class="fa-solid fa-code-branch"></i> Versions
20 20 </.link>
21 21 </form>
22 − <div class="my-2 flex flex-wrap items-center gap-4">
23 − <span class="text-sm opacity-60">
24 − <i class="fa-solid fa-network-wired fa-fw"></i> Sweep
25 − </span>
26 − <%= for iface <- @interfaces do %>
27 − <label class="label cursor-pointer gap-2 p-0">
28 − <input
29 − type="checkbox"
30 − class="checkbox checkbox-sm"
31 − checked={iface.enabled}
32 − phx-click="toggle_iface"
33 − phx-value-name={iface.name}
34 − />
35 − <span class="label-text">
36 − <%= iface.name %>
37 − <%= if iface.addr do %>
38 − <span class="opacity-60"><%= iface.addr %></span>
39 − <% end %>
40 − </span>
41 − </label>
42 − <% end %>
43 − <%= if @interfaces == [] do %>
44 − <span class="text-sm opacity-60">No interfaces to scan.</span>
45 − <% end %>
22 + <div class="collapse collapse-arrow border border-base-300 bg-base-100 rounded-box my-2">
23 + <input type="checkbox" />
24 + <div class="collapse-title text-sm opacity-60">
25 + <i class="fa-solid fa-network-wired fa-fw"></i>
26 + Sweep
27 + <span class="opacity-80">
28 + <%= @interfaces |> Enum.filter(& &1.enabled) |> length %> of <%= length(@interfaces) %> interfaces
29 + </span>
30 + </div>
31 + <div class="collapse-content">
32 + <div class="flex flex-wrap items-center gap-4">
33 + <%= for iface <- @interfaces do %>
34 + <label class="label cursor-pointer gap-2 p-0">
35 + <input
36 + type="checkbox"
37 + class="checkbox checkbox-sm"
38 + checked={iface.enabled}
39 + phx-click="toggle_iface"
40 + phx-value-name={iface.name}
41 + />
42 + <span class="label-text">
43 + <%= iface.name %>
44 + <%= if iface.addr do %>
45 + <span class="opacity-60"><%= iface.addr %></span>
46 + <% end %>
47 + </span>
48 + </label>
49 + <% end %>
50 + <%= if @interfaces == [] do %>
51 + <span class="text-sm opacity-60">No interfaces to scan.</span>
52 + <% end %>
53 + </div>
54 + </div>
46 55 </div>
47 56 <div class="hidden lg:block">
48 57 <table class="table table-zebra w-full">
modified test/lambent_ex_web/live/interfaces_test.exs
+3 −0
@@ -16,6 +16,9 @@ defmodule LambentExWeb.InterfacesTest do
16 16
17 17 assert html =~ "Sweep"
18 18 assert html =~ "toggle_iface"
19 + # folded away, with a count of what is on so it reads at a glance
20 + assert html =~ "collapse"
21 + assert html =~ "of #{length(NMAP.interfaces())} interfaces"
19 22
20 23 # whatever this machine has, minus the prefixes the scanner drops
21 24 for iface <- NMAP.interfaces() do

Parents: 7a73ed3