stopou_blocker 0.6.1 copy "stopou_blocker: ^0.6.1" to clipboard
stopou_blocker: ^0.6.1 copied to clipboard

Plugin do Stopou para bloqueio por VPN local (preparado para estratégias futuras).

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:stopou_blocker/stopou_blocker.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Stopou Blocker Demo',
      theme: ThemeData(useMaterial3: true),
      home: const DemoPage(),
    );
  }
}

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});
  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  final _log = <String>[];
  bool _vpnRunning = false;
  bool _keywordBlockerRunning = false;
  bool _hasVpnPermission = false;
  bool _hasAccessibilityPermission = false;
  bool _hasNotificationPermission = false;

  @override
  void initState() {
    super.initState();
    // ✅ Assina o stream de eventos do plugin.
    StopouBlocker.events.listen((e) {
      setState(() {
        _log.add(
          "${e.ts.toIso8601String()}   ${e.protocol.padRight(5)}   ${e.host}   ${e.appPackage ?? ''}",
        );
      });
    });

    // ✅ Verifica status inicial
    _updateStatus();
  }

  Future<void> _updateStatus() async {
    final vpnRunning = await StopouBlocker.isVpnRunning();
    final keywordRunning = await StopouBlocker.isKeywordBlockerRunning();
    final hasVpn = await StopouBlocker.hasVpnPermission();
    final hasAccessibility = await StopouBlocker.hasAccessibilityPermission();
    final hasNotification = await StopouBlocker.hasNotificationPermission();

    setState(() {
      _vpnRunning = vpnRunning;
      _keywordBlockerRunning = keywordRunning;
      _hasVpnPermission = hasVpn;
      _hasAccessibilityPermission = hasAccessibility;
      _hasNotificationPermission = hasNotification;
    });
  }

  /// 📝 Adiciona uma mensagem ao log
  void _addLog(String message) {
    final timestamp = DateTime.now().toString().substring(11, 19);
    setState(() {
      _log.add('[$timestamp] $message');
    });
    // Também exibe no console para debugging
    print('[$timestamp] $message');
  }

  /// 📱 Testa a funcionalidade de obter aplicativos instalados
  Future<void> _testInstalledApps() async {
    _addLog('📱 [TEST] Iniciando teste de aplicativos instalados...');
    
    try {
      final stopwatch = Stopwatch()..start();
      
      // Obtém lista de aplicativos instalados (formato JSON)
      final apps = await StopouBlocker.getInstalledApps();
      
      stopwatch.stop();
      final duration = stopwatch.elapsedMilliseconds;
      
      _addLog('📱 [TEST] ✅ Aplicativos obtidos em ${duration}ms');
      _addLog('📱 [TEST] Total de aplicativos: ${apps.length}');
      _addLog('📱 [TEST] 📋 Formato JSON limpo: {"label": "...", "packageName": "...", "iconBase64": "..." ou null}');
      
      _addLog('📱 [TEST] ========== PRIMEIROS 10 APLICATIVOS ==========');
      
      // Exibe os primeiros 10 aplicativos para validar
      final appsToShow = apps.take(10);
      for (int i = 0; i < appsToShow.length; i++) {
        final app = appsToShow.elementAt(i);
        final label = app['label'] as String? ?? '';
        final packageName = app['packageName'] as String? ?? '';
        final iconBase64 = app['iconBase64'] as String?;
        final hasIcon = iconBase64 != null ? '🎨' : '🚫';
        
        _addLog('📱 [TEST] ${i + 1}. $hasIcon $label');
        _addLog('📱 [TEST]    📦 $packageName');
        
        // Validar dados
        if (label.isEmpty) {
          _addLog('📱 [TEST] ⚠️ App sem label: $packageName');
        }
        if (packageName.isEmpty) {
          _addLog('📱 [TEST] ❌ App sem packageName!');
        }
      }
      
      _addLog('📱 [TEST] ========================================');
      
      // Estatísticas
      final appsWithIcons = apps.where((app) => app['iconBase64'] != null).length;
      final appsWithoutIcons = apps.length - appsWithIcons;
      
      _addLog('📱 [TEST] 📊 Com ícones: $appsWithIcons');
      _addLog('📱 [TEST] 📊 Sem ícones: $appsWithoutIcons');
      
      // Validar dados críticos
      final appsWithEmptyLabel = apps.where((app) => (app['label'] as String? ?? '').isEmpty).length;
      final appsWithEmptyPackage = apps.where((app) => (app['packageName'] as String? ?? '').isEmpty).length;
      
      if (appsWithEmptyLabel > 0) {
        _addLog('📱 [TEST] ⚠️ $appsWithEmptyLabel apps com label vazio');
      }
      
      if (appsWithEmptyPackage > 0) {
        _addLog('📱 [TEST] ❌ $appsWithEmptyPackage apps com packageName vazio');
      }
      
      _addLog('📱 [TEST] ✅ Teste concluído com sucesso!');
      
      // Exibir diálogo com resumo
      if (mounted) {
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('📱 Aplicativos Instalados'),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Total: ${apps.length} aplicativos'),
                Text('Tempo: ${duration}ms'),
                Text('Com ícones: $appsWithIcons'),
                Text('Sem ícones: $appsWithoutIcons'),
                const SizedBox(height: 8),
                const Text('✅ Formato JSON compatível com FlutterFlow',
                  style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)),
                if (appsWithEmptyLabel > 0)
                  Text('⚠️ $appsWithEmptyLabel sem label', 
                      style: const TextStyle(color: Colors.orange)),
                if (appsWithEmptyPackage > 0)
                  Text('❌ $appsWithEmptyPackage sem package', 
                      style: const TextStyle(color: Colors.red)),
              ],
            ),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text('OK'),
              ),
            ],
          ),
        );
      }
      
    } catch (e, stackTrace) {
      _addLog('📱 [TEST] ❌ Erro ao obter aplicativos: $e');
      _addLog('📱 [TEST] Stack trace: $stackTrace');
      
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('❌ Erro: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  /// 🧪 Testa a funcionalidade completa de bloqueio de aplicativos
  Future<void> _testAppBlocker() async {
    _addLog('🧪 [APP_BLOCKER] ========== TESTE DE BLOQUEIO DE APPS ==========');
    
    try {
      // 1. Verificar permissão de acessibilidade
      _addLog('🧪 [APP_BLOCKER] 1. Verificando permissões...');
      final hasAccessibility = await StopouBlocker.hasAccessibilityPermission();
      _addLog('🧪 [APP_BLOCKER] Acessibilidade: ${hasAccessibility ? "✅" : "❌"}');
      
      if (!hasAccessibility) {
        _addLog('🧪 [APP_BLOCKER] ⚠️ Permissão de acessibilidade necessária');
        _addLog('🧪 [APP_BLOCKER] 💡 Use o botão "Solicitar Permissões" primeiro');
        return;
      }
      
      // 2. Obter lista de apps para teste
      _addLog('🧪 [APP_BLOCKER] 2. Obtendo apps instalados...');
      final apps = await StopouBlocker.getInstalledApps();
      _addLog('🧪 [APP_BLOCKER] Encontrados: ${apps.length} apps');
      
      // 3. Selecionar app de teste (Chrome preferencialmente)
      String? testPackage;
      final preferredPackages = [
        'com.android.chrome',
        'com.google.android.youtube',
        'com.whatsapp'
      ];
      
      for (final pkg in preferredPackages) {
        if (apps.any((app) => app['packageName'] == pkg)) {
          testPackage = pkg;
          break;
        }
      }
      
      if (testPackage == null && apps.isNotEmpty) {
        testPackage = apps.first['packageName'] as String;
      }
      
      if (testPackage == null) {
        _addLog('🧪 [APP_BLOCKER] ❌ Nenhum app encontrado para teste');
        return;
      }
      
      final testAppName = apps.firstWhere(
        (app) => app['packageName'] == testPackage,
        orElse: () => {'label': 'App Desconhecido'}
      )['label'] as String;
      
      _addLog('🧪 [APP_BLOCKER] 3. App selecionado: $testAppName ($testPackage)');
      
      // 4. Testar início do bloqueio
      _addLog('🧪 [APP_BLOCKER] 4. Iniciando bloqueio...');
      final startResult = await StopouBlocker.startAppBlocker([testPackage]);
      _addLog('🧪 [APP_BLOCKER] Resultado início: ${startResult ? "✅" : "❌"}');
      
      if (!startResult) {
        _addLog('🧪 [APP_BLOCKER] ❌ Falha ao iniciar bloqueio');
        return;
      }
      
      // 5. Aguardar um pouco para testar
      _addLog('🧪 [APP_BLOCKER] 5. Aguardando 3 segundos para teste...');
      _addLog('🧪 [APP_BLOCKER] 🎯 AGORA! Tente abrir o app: $testAppName');
      _addLog('🧪 [APP_BLOCKER] 💡 O app deve fechar automaticamente!');
      
      await Future.delayed(const Duration(seconds: 3));
      
      // 6. Parar bloqueio
      _addLog('🧪 [APP_BLOCKER] 6. Parando bloqueio...');
      final stopResult = await StopouBlocker.stopAppBlocker();
      _addLog('🧪 [APP_BLOCKER] Resultado parada: ${stopResult ? "✅" : "❌"}');
      
      // 7. Resultado final
      if (startResult && stopResult) {
        _addLog('🧪 [APP_BLOCKER] ✅ TESTE CONCLUÍDO COM SUCESSO!');
        _addLog('🧪 [APP_BLOCKER] 💡 Sistema funcionando corretamente');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('✅ Teste de bloqueio concluído com sucesso!'),
              backgroundColor: Colors.green,
            ),
          );
        }
      } else {
        _addLog('🧪 [APP_BLOCKER] ❌ TESTE FALHOU');
        _addLog('🧪 [APP_BLOCKER] Início: $startResult, Parada: $stopResult');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('❌ Teste de bloqueio falhou'),
              backgroundColor: Colors.red,
            ),
          );
        }
      }
      
    } catch (e, stackTrace) {
      _addLog('🧪 [APP_BLOCKER] ❌ Erro no teste: $e');
      _addLog('🧪 [APP_BLOCKER] Stack trace: $stackTrace');
      
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('❌ Erro no teste: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
    
    _addLog('🧪 [APP_BLOCKER] ==========================================');
  }

  /// 🚫 Demo: Inicia bloqueio do Chrome
  Future<void> _startAppBlockerDemo() async {
    _addLog('🚫 [DEMO] Iniciando bloqueio do Chrome...');
    
    try {
      // Verificar permissão
      final hasPermission = await StopouBlocker.hasAccessibilityPermission();
      if (!hasPermission) {
        _addLog('🚫 [DEMO] ❌ Permissão de acessibilidade necessária');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('❌ Permissão de acessibilidade necessária'),
              backgroundColor: Colors.red,
            ),
          );
        }
        return;
      }
      
      // Iniciar bloqueio do Chrome
      const chromePackage = 'com.android.chrome';
      final success = await StopouBlocker.startAppBlocker([chromePackage]);
      
      if (success) {
        _addLog('🚫 [DEMO] ✅ Chrome bloqueado!');
        _addLog('🚫 [DEMO] 🎯 Tente abrir o Chrome - ele deve fechar automaticamente');
        _addLog('🚫 [DEMO] 💡 Use "Demo: Parar Bloqueio" para desbloquear');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('🚫 Chrome bloqueado! Tente abrí-lo para testar'),
              backgroundColor: Colors.orange,
              duration: Duration(seconds: 4),
            ),
          );
        }
      } else {
        _addLog('🚫 [DEMO] ❌ Falha ao bloquear Chrome');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('❌ Falha ao bloquear Chrome'),
              backgroundColor: Colors.red,
            ),
          );
        }
      }
      
    } catch (e) {
      _addLog('🚫 [DEMO] ❌ Erro: $e');
      
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('❌ Erro: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  /// ✅ Demo: Para bloqueio de aplicativos
  Future<void> _stopAppBlockerDemo() async {
    _addLog('✅ [DEMO] Parando bloqueio de aplicativos...');
    
    try {
      final success = await StopouBlocker.stopAppBlocker();
      
      if (success) {
        _addLog('✅ [DEMO] ✅ Bloqueio parado!');
        _addLog('✅ [DEMO] 🎯 Todos os apps podem ser abertos normalmente');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('✅ Bloqueio parado! Apps liberados'),
              backgroundColor: Colors.green,
            ),
          );
        }
      } else {
        _addLog('✅ [DEMO] ❌ Falha ao parar bloqueio');
        
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('❌ Falha ao parar bloqueio'),
              backgroundColor: Colors.red,
            ),
          );
        }
      }
      
    } catch (e) {
      _addLog('✅ [DEMO] ❌ Erro: $e');
      
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('❌ Erro: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  void _showAccessibilityInstructions() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('⚠️ Permissão de Acessibilidade'),
        content: const SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Se o Android mostrar "permissão restrita", siga estes passos:',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 12),
              Text('1. Nas configurações de Acessibilidade, encontre "Stopou"'),
              SizedBox(height: 8),
              Text('2. Toque em "Stopou" para abrir as configurações'),
              SizedBox(height: 8),
              Text(
                '3. Se aparecer aviso de "permissão restrita", toque em "Aprenda a conceder acesso"',
              ),
              SizedBox(height: 8),
              Text('4. Ative o toggle para "Usar Stopou"'),
              SizedBox(height: 8),
              Text('5. Confirme tocando em "OK" quando perguntado'),
              SizedBox(height: 12),
              Text(
                'ℹ️ O Stopou é seguro: apenas detecta palavras específicas e exibe alertas. Não coleta dados pessoais.',
                style: TextStyle(fontStyle: FontStyle.italic),
              ),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Entendi'),
          ),
          ElevatedButton(
            onPressed: () async {
              Navigator.of(context).pop();
              await StopouBlocker.openAccessibilitySettings();
            },
            child: const Text('Abrir Configurações'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Stopou Blocker Demo'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _updateStatus,
            tooltip: 'Atualizar Status',
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Status Section
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      '📊 Status dos Serviços',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 12),
                    Row(
                      children: [
                        Icon(
                          _vpnRunning
                              ? Icons.vpn_lock
                              : Icons.vpn_lock_outlined,
                          color: _vpnRunning ? Colors.green : Colors.grey,
                        ),
                        const SizedBox(width: 8),
                        Text('VPN: ${_vpnRunning ? "Ativo" : "Inativo"}'),
                      ],
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        Icon(
                          _keywordBlockerRunning
                              ? Icons.accessibility
                              : Icons.accessibility_outlined,
                          color: _keywordBlockerRunning
                              ? Colors.green
                              : Colors.grey,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          'Bloqueador Keywords: ${_keywordBlockerRunning ? "Ativo" : "Inativo"}',
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Permissions Section
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      '🔑 Permissões',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 12),

                    ElevatedButton.icon(
                      onPressed: () async {
                        final ok = await StopouBlocker.requestPermission();
                        await _updateStatus();
                        if (mounted) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text('Permissão VPN: $ok')),
                          );
                        }
                      },
                      icon: Icon(
                        _hasVpnPermission ? Icons.check : Icons.vpn_key,
                      ),
                      label: Text('VPN ${_hasVpnPermission ? "✓" : "✗"}'),
                    ),
                    const SizedBox(height: 8),

                    ElevatedButton.icon(
                      onPressed: () async {
                        final ok =
                            await StopouBlocker.requestAccessibilityPermission();
                        await _updateStatus();
                        if (mounted) {
                          if (!ok) {
                            _showAccessibilityInstructions();
                          } else {
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(
                                content: Text('Permissão Acessibilidade: ✓'),
                              ),
                            );
                          }
                        }
                      },
                      icon: Icon(
                        _hasAccessibilityPermission
                            ? Icons.check
                            : Icons.accessibility,
                      ),
                      label: Text(
                        'Acessibilidade ${_hasAccessibilityPermission ? "✓" : "✗"}',
                      ),
                    ),
                    const SizedBox(height: 8),

                    ElevatedButton.icon(
                      onPressed: () async {
                        final ok =
                            await StopouBlocker.requestNotificationPermission();
                        await _updateStatus();
                        if (mounted) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text('Permissão Notificação: $ok'),
                            ),
                          );
                        }
                      },
                      icon: Icon(
                        _hasNotificationPermission
                            ? Icons.check
                            : Icons.notifications,
                      ),
                      label: Text(
                        'Notificações ${_hasNotificationPermission ? "✓" : "✗"}',
                      ),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Controls Section
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      '🎛️ Controles',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 12),

                    ElevatedButton.icon(
                      onPressed: _vpnRunning
                          ? null
                          : () async {
                              await StopouBlocker.start(
                                blocklist: const ['.bet.br', 'exemplo.com'],
                                logAttempts: true,
                                dnsServers: const ['1.1.1.1', '8.8.8.8'],
                                strategies: const [BlockStrategies.vpn],
                              );
                              await _updateStatus();
                            },
                      icon: const Icon(Icons.play_arrow),
                      label: const Text('Iniciar VPN'),
                    ),
                    const SizedBox(height: 8),

                    ElevatedButton.icon(
                      onPressed: _keywordBlockerRunning
                          ? null
                          : () async {
                              await StopouBlocker.startKeywordBlocker([
                                'bet',
                                'casino',
                                'apostas',
                              ]);
                              await _updateStatus();
                            },
                      icon: const Icon(Icons.block),
                      label: const Text('Iniciar Bloqueador Keywords'),
                    ),
                    const SizedBox(height: 8),

                    ElevatedButton.icon(
                      onPressed: (!_vpnRunning && !_keywordBlockerRunning)
                          ? null
                          : () async {
                              await StopouBlocker.stop();
                              await StopouBlocker.stopKeywordBlocker();
                              await _updateStatus();
                            },
                      icon: const Icon(Icons.stop),
                      label: const Text('Parar Tudo'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.red[100],
                      ),
                    ),
                    const SizedBox(height: 16),
                    
                    // Seção de teste de aplicativos instalados
                    const Divider(),
                    const Text('🧪 Testes', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    
                    ElevatedButton.icon(
                      onPressed: _testInstalledApps,
                      icon: const Icon(Icons.apps),
                      label: const Text('Testar Apps Instalados'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.purple,
                        foregroundColor: Colors.white,
                      ),
                    ),
                    const SizedBox(height: 8),
                    
                    ElevatedButton.icon(
                      onPressed: _testAppBlocker,
                      icon: const Icon(Icons.block),
                      label: const Text('Testar Bloqueio Apps'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.orange,
                        foregroundColor: Colors.white,
                      ),
                    ),
                    const SizedBox(height: 8),
                    
                    ElevatedButton.icon(
                      onPressed: _startAppBlockerDemo,
                      icon: const Icon(Icons.security),
                      label: const Text('Demo: Bloquear Chrome'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.red,
                        foregroundColor: Colors.white,
                      ),
                    ),
                    const SizedBox(height: 8),
                    
                    ElevatedButton.icon(
                      onPressed: _stopAppBlockerDemo,
                      icon: const Icon(Icons.lock_open),
                      label: const Text('Demo: Parar Bloqueio'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.green,
                        foregroundColor: Colors.white,
                      ),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Events Log
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                '📝 Log de Eventos:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
            const SizedBox(height: 8),
            Expanded(
              child: Container(
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey.shade300),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: _log.isEmpty
                    ? const Center(
                        child: Text('Nenhum evento registrado ainda...'),
                      )
                    : ListView.builder(
                        itemCount: _log.length,
                        itemBuilder: (_, i) => Padding(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 4,
                          ),
                          child: Text(
                            _log[_log.length - 1 - i], // Mais recentes primeiro
                            style: const TextStyle(
                              fontFamily: 'monospace',
                              fontSize: 12,
                            ),
                          ),
                        ),
                      ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
0
points
9
downloads

Publisher

unverified uploader

Weekly Downloads

Plugin do Stopou para bloqueio por VPN local (preparado para estratégias futuras).

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on stopou_blocker

Packages that implement stopou_blocker