Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/todos/lib/controllers/todos.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ final todosControllerProvider = Provider<TodosController>(
class TodosController {
TodosController({
List<Todo> initialTodos = const [],
}) : todos = ListSignal(initialTodos);
}) : todos = ListSignal(initialTodos, name: 'todos');

// The list of todos
final ListSignal<Todo> todos;

/// The list of completed todos
late final completedTodos = Computed(
() => todos.where((todo) => todo.completed).toList(),
name: 'completedTodos',
);

/// The list of incomplete todos
late final incompleteTodos = Computed(
() => todos.where((todo) => !todo.completed).toList(),
name: 'incompleteTodos',
);

/// Add a todo
Expand Down
4 changes: 3 additions & 1 deletion examples/todos/lib/widgets/todos_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import 'package:solidart_example/domain/todo.dart';
import 'package:solidart_example/widgets/todos_list.dart';
import 'package:solidart_example/widgets/toolbar.dart';

final todosFilterProvider = Provider((context) => Signal(TodosFilter.all));
final todosFilterProvider = Provider(
(context) => Signal(TodosFilter.all, name: 'todosFilter'),
);

class TodosBody extends StatefulWidget {
const TodosBody({super.key});
Expand Down
7 changes: 6 additions & 1 deletion examples/todos/lib/widgets/toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ class _ToolbarState extends State<Toolbar> {

/// All the derived signals, they will react only when the `length` property
/// changes
late final allTodosCount = Computed(() => todosController.todos().length);
late final allTodosCount = Computed(
() => todosController.todos().length,
name: 'allTodosCount',
);
late final incompleteTodosCount = Computed(
() => todosController.incompleteTodos().length,
name: 'incompleteTodosCount',
);
late final completedTodosCount = Computed(
() => todosController.completedTodos().length,
name: 'completedTodosCount',
);

@override
Expand Down
129 changes: 90 additions & 39 deletions packages/solidart_devtools_extension/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,21 @@ class SignalData {

class _SignalsState extends State<Signals> {
late final StreamSubscription<Object>? sub;
late final StreamSubscription<Object>? isolateEventSub;
final selectedSignalId = Signal<String?>(null);
final searchController = SearchController();
final searchText = Signal<String>('');
final filterType = Signal<SignalType?>(null);
final showDisposed = Signal<bool>(true);
final showOnlyActiveDuplicates = Signal<bool>(false);
final signals = MapSignal<String, SignalData>({});

late final filteredSignals = Computed(() {
final lowercasedSearch = searchText.value.toLowerCase();
final type = filterType.value;
final viewDisposed = showDisposed.value;
return signals.value.entries
final onlyActiveDuplicates = showOnlyActiveDuplicates.value;
final filtered = signals.value.entries
.where(
(entry) =>
entry.value.name.toString().toLowerCase().contains(
Expand All @@ -121,7 +124,27 @@ class _SignalsState extends State<Signals> {
)
.where((entry) => type == null || entry.value.type == type)
.where((entry) => viewDisposed || !entry.value.disposed)
.where((entry) {
if (!onlyActiveDuplicates) return true;
// Count signals with same name
final sameName = signals.value.values
.where((s) => s.name == entry.value.name);
// Show only if: has duplicates AND is active
return sameName.length > 1 && !entry.value.disposed;
})
.toList();

// Sort by lastUpdate (newest first), then by disposed status (active first)
filtered.sort((a, b) {
final disposedCompare = a.value.disposed ? 1 : -1;
final disposedCompare2 = b.value.disposed ? 1 : -1;
if (disposedCompare != disposedCompare2) {
return disposedCompare.compareTo(disposedCompare2);
}
return b.value.lastUpdate.compareTo(a.value.lastUpdate);
});

return filtered;
});

@override
Expand Down Expand Up @@ -158,6 +181,14 @@ class _SignalsState extends State<Signals> {
);
}
});

// Listen for hot restart events to clear signals
isolateEventSub = vmService.onIsolateEvent.listen((event) {
if (event.kind == 'IsolateReload' || event.kind == 'IsolateStart') {
signals.clear();
}
});

searchController.addListener(
() => searchText.value = searchController.text,
);
Expand All @@ -166,7 +197,15 @@ class _SignalsState extends State<Signals> {
@override
void dispose() {
sub?.cancel();
isolateEventSub?.cancel();
searchController.dispose();
selectedSignalId.dispose();
searchText.dispose();
filterType.dispose();
showDisposed.dispose();
showOnlyActiveDuplicates.dispose();
signals.dispose();
filteredSignals.dispose();
super.dispose();
}

Expand Down Expand Up @@ -206,30 +245,33 @@ class _SignalsState extends State<Signals> {
spacing: 2,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShadInput(
placeholder: Text('Search signals'),
controller: searchController,
trailing: Show(
when: () => searchText.value.isNotEmpty,
builder: (context) {
return ShadIconButton(
onPressed: searchController.clear,
width: 20,
height: 20,
padding: EdgeInsets.zero,
decoration: const ShadDecoration(
secondaryBorder: ShadBorder.none,
secondaryFocusedBorder: ShadBorder.none,
),
icon: const Icon(Icons.clear, size: 14),
);
},
),
),
const SizedBox(height: 8),
Row(
spacing: 8,
children: [
Flexible(
Expanded(
flex: 2,
child: ShadInput(
placeholder: Text('Search signals'),
controller: searchController,
trailing: Show(
when: () => searchText.value.isNotEmpty,
builder: (context) {
return ShadIconButton(
onPressed: searchController.clear,
width: 20,
height: 20,
padding: EdgeInsets.zero,
decoration: const ShadDecoration(
secondaryBorder: ShadBorder.none,
secondaryFocusedBorder: ShadBorder.none,
),
icon: const Icon(Icons.clear, size: 14),
);
},
),
),
),
Expanded(
child: SignalBuilder(
builder: (context, _) {
return ShadSelect<SignalType>(
Expand All @@ -254,22 +296,39 @@ class _SignalsState extends State<Signals> {
},
),
),
SizedBox(
height: 20,
child: const ShadSeparator.vertical(),
),
Flexible(
],
),
const SizedBox(height: 8),
Row(
spacing: 8,
children: [
Expanded(
child: SignalBuilder(
builder: (context, _) {
return ShadCheckbox(
value: showDisposed.value,
label: const Text('Show disposed'),
padding: EdgeInsets.only(left: 4),
onChanged: (v) => showDisposed.value = v,
);
},
),
),
Expanded(
child: SignalBuilder(
builder: (context, _) {
return ShadCheckbox(
value: showOnlyActiveDuplicates.value,
label: const Text('Active duplicates'),
onChanged: (v) => showOnlyActiveDuplicates.value = v,
);
},
),
),
ShadButton.outline(
onPressed: () => signals.clear(),
size: ShadButtonSize.sm,
child: const Text('Clear All'),
),
],
),
const SizedBox(height: 4),
Expand All @@ -278,7 +337,7 @@ class _SignalsState extends State<Signals> {
return Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'${filteredSignals.value.length} visible of ${signals.value.length}',
'${filteredSignals.value.length} visible of ${signals.value.length}${showOnlyActiveDuplicates.value ? ' (duplicates only)' : ''}',
style: shadTheme.textTheme.muted,
),
);
Expand All @@ -299,15 +358,7 @@ class _SignalsState extends State<Signals> {
itemCount: filteredSignals.value.length,
padding: EdgeInsets.symmetric(horizontal: 4),
itemBuilder: (BuildContext context, int index) {
final sortedSignals = filteredSignals.value
..sort(
(a, b) => b.value.lastUpdate.compareTo(
a.value.lastUpdate,
),
)
..sort((a, b) => a.value.disposed ? 1 : -1);

final entry = sortedSignals.elementAt(index);
final entry = filteredSignals.value[index];
final name = entry.value.name;
final signal = entry.value;
return SignalBuilder(
Expand Down
Loading