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

PlatformAndroid

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: {"label": "...", "packageName": "...", "icon": "data:image/png;base64,..." ou null}');
      
      _addLog('📱 [TEST] ========== PRIMEIROS 10 APLICATIVOS DO USUÁRIO ==========');
      
      // 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 icon = app['icon'] as String?;
        final hasIcon = icon != null ? '🎨' : '🚫';
        final hasPrefix = icon?.startsWith('data:image/png;base64,') == true ? '✅' : '❌';
        
        _addLog('📱 [TEST] ${i + 1}. $hasIcon $label');
        _addLog('📱 [TEST]    📦 $packageName');
        _addLog('📱 [TEST]    🔗 Prefixo data: $hasPrefix');
        
        // 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
150
points
170
downloads

Publisher

unverified uploader

Weekly Downloads

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

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on stopou_blocker

Packages that implement stopou_blocker