no_screenshot 0.8.0 copy "no_screenshot: ^0.8.0" to clipboard
no_screenshot: ^0.8.0 copied to clipboard

Flutter plugin to prevent screenshots, detect screen recording, and show blur/color/image overlays in the app switcher on Android, iOS, macOS, Linux, Windows, and Web.

example/lib/main.dart

import 'dart:async';
import 'dart:io' show Platform;

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:no_screenshot/overlay_mode.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:no_screenshot/secure_navigator_observer.dart';
import 'package:no_screenshot/secure_widget.dart';

import 'app_localizations.dart';

// Screenshot prevention (off/on/toggle): works on Android, iOS, macOS,
// Windows, and Web (best-effort). No-op on Linux.
bool get _supportsProtection => kIsWeb || !Platform.isLinux;

// Visual overlays (image, blur, color) in app switcher: only Android, iOS,
// macOS show actual overlays. Windows/Linux/Web just track state or toggle
// protection without a visual overlay.
bool get _supportsOverlays =>
    !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);

// Recording monitoring: works on Android, iOS, macOS, Windows, Linux.
// No-op on Web.
bool get _supportsRecordingMonitoring => !kIsWeb;

void main() {
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isRTL = false;

  void _toggleRTL(bool value) {
    setState(() => _isRTL = value);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'No Screenshot Example',
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
      ),
      locale: _isRTL ? const Locale('ar') : const Locale('en'),
      supportedLocales: AppLocalizations.supportedLocales,
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      home: HomePage(isRTL: _isRTL, onRTLChanged: _toggleRTL),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key, required this.isRTL, required this.onRTLChanged});

  final bool isRTL;
  final ValueChanged<bool> onRTLChanged;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _noScreenshot = NoScreenshot.instance;
  StreamSubscription<ScreenshotSnapshot>? _streamSubscription;
  bool _isMonitoring = false;
  bool _isRecordingMonitoring = false;
  bool _isOverlayImageOn = false;
  bool _isOverlayBlurOn = false;
  bool _isOverlayColorOn = false;
  bool _callbacksActive = false;
  String _lastCallbackEvent = '';
  ScreenshotSnapshot _latestSnapshot = ScreenshotSnapshot(
    isScreenshotProtectionOn: false,
    wasScreenshotTaken: false,
    screenshotPath: '',
  );

  @override
  void initState() {
    super.initState();
    _streamSubscription = _noScreenshot.screenshotStream.listen((value) {
      if (!mounted) return;
      setState(() => _latestSnapshot = value);
      if (value.wasScreenshotTaken) {
        debugPrint('Screenshot taken at path: ${value.screenshotPath}');
        _showScreenshotAlert(value);
      }
    });

    // Set up granular callbacks (P15)
    _noScreenshot.onScreenshotDetected = (snapshot) {
      debugPrint('Callback: screenshot detected');
      if (!mounted) return;
      setState(() => _lastCallbackEvent = 'Screenshot detected');
    };
    _noScreenshot.onScreenRecordingStarted = (snapshot) {
      debugPrint('Callback: recording started');
      if (!mounted) return;
      setState(() => _lastCallbackEvent = 'Recording started');
    };
    _noScreenshot.onScreenRecordingStopped = (snapshot) {
      debugPrint('Callback: recording stopped');
      if (!mounted) return;
      setState(() => _lastCallbackEvent = 'Recording stopped');
    };
    _noScreenshot.startCallbacks();
    _callbacksActive = true;
  }

  @override
  void dispose() {
    _streamSubscription?.cancel();
    _noScreenshot.removeAllCallbacks();
    super.dispose();
  }

  // ── Screenshot Protection ──────────────────────────────────────────

  Future<void> _disableScreenshot() async {
    final result = await _noScreenshot.screenshotOff();
    debugPrint('screenshotOff: $result');
  }

  Future<void> _enableScreenshot() async {
    final result = await _noScreenshot.screenshotOn();
    debugPrint('screenshotOn: $result');
  }

  Future<void> _toggleScreenshot() async {
    final result = await _noScreenshot.toggleScreenshot();
    debugPrint('toggleScreenshot: $result');
  }

  // ── Screenshot Monitoring ──────────────────────────────────────────

  Future<void> _startMonitoring() async {
    await _noScreenshot.startScreenshotListening();
    setState(() => _isMonitoring = true);
  }

  Future<void> _stopMonitoring() async {
    await _noScreenshot.stopScreenshotListening();
    setState(() => _isMonitoring = false);
  }

  // ── Recording Monitoring ───────────────────────────────────────────

  Future<void> _startRecordingMonitoring() async {
    await _noScreenshot.startScreenRecordingListening();
    setState(() => _isRecordingMonitoring = true);
  }

  Future<void> _stopRecordingMonitoring() async {
    await _noScreenshot.stopScreenRecordingListening();
    setState(() => _isRecordingMonitoring = false);
  }

  // ── Set Overlay Image ──────────────────────────────────────────────

  Future<void> _toggleScreenshotWithImage() async {
    final result = await _noScreenshot.toggleScreenshotWithImage();
    debugPrint('toggleScreenshotWithImage: $result');
    setState(() {
      _isOverlayImageOn = result;
      if (result) {
        _isOverlayBlurOn = false;
        _isOverlayColorOn = false;
      }
    });
  }

  Future<void> _toggleScreenshotWithBlur() async {
    final result = await _noScreenshot.toggleScreenshotWithBlur();
    debugPrint('toggleScreenshotWithBlur: $result');
    setState(() {
      _isOverlayBlurOn = result;
      if (result) {
        _isOverlayImageOn = false;
        _isOverlayColorOn = false;
      }
    });
  }

  Future<void> _toggleScreenshotWithColor() async {
    final result = await _noScreenshot.toggleScreenshotWithColor();
    debugPrint('toggleScreenshotWithColor: $result');
    setState(() {
      _isOverlayColorOn = result;
      if (result) {
        _isOverlayImageOn = false;
        _isOverlayBlurOn = false;
      }
    });
  }

  // ── UI ─────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(l.appTitle),
        actions: [
          Text(l.rtl),
          Switch(
            value: widget.isRTL,
            onChanged: widget.onRTLChanged,
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          if (_supportsProtection) ...[
            _buildSection(
              title: l.protectionSectionTitle,
              subtitle: l.platformSubtitle,
              children: [
                _StatusRow(
                  label: l.protection,
                  isOn: _latestSnapshot.isScreenshotProtectionOn,
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      child: _FeatureButton(
                        label: l.disableScreenshot,
                        subtitle: l.blocksCapture,
                        onPressed: _disableScreenshot,
                      ),
                    ),
                    const SizedBox(width: 8),
                    Expanded(
                      child: _FeatureButton(
                        label: l.enableScreenshot,
                        subtitle: l.allowsCapture,
                        onPressed: _enableScreenshot,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                _FeatureButton(
                  label: l.toggleScreenshot,
                  subtitle: l.toggleScreenshotSubtitle,
                  onPressed: _toggleScreenshot,
                ),
              ],
            ),
            const SizedBox(height: 16),
          ],
          _buildSection(
            title: l.monitoringSectionTitle,
            subtitle: l.platformSubtitle,
            children: [
              _StatusRow(
                label: l.monitoring,
                isOn: _isMonitoring,
              ),
              const SizedBox(height: 8),
              _SnapshotInfo(snapshot: _latestSnapshot),
              const SizedBox(height: 12),
              Row(
                children: [
                  Expanded(
                    child: _FeatureButton(
                      label: l.enableMonitoring,
                      subtitle: l.startListening,
                      onPressed: _startMonitoring,
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: _FeatureButton(
                      label: l.disableMonitoring,
                      subtitle: l.stopListening,
                      onPressed: _stopMonitoring,
                    ),
                  ),
                ],
              ),
            ],
          ),
          if (_supportsRecordingMonitoring) ...[
            const SizedBox(height: 16),
            _buildSection(
              title: l.recordingMonitoringSectionTitle,
              subtitle: l.platformSubtitle,
              children: [
                _StatusRow(
                  label: l.recordingMonitoring,
                  isOn: _isRecordingMonitoring,
                ),
                const SizedBox(height: 8),
                _StatusRow(
                  label: l.screenRecording,
                  isOn: _latestSnapshot.isScreenRecording,
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      child: _FeatureButton(
                        label: l.enableRecordingMonitoring,
                        subtitle: l.startRecordingListening,
                        onPressed: _startRecordingMonitoring,
                      ),
                    ),
                    const SizedBox(width: 8),
                    Expanded(
                      child: _FeatureButton(
                        label: l.disableRecordingMonitoring,
                        subtitle: l.stopRecordingListening,
                        onPressed: _stopRecordingMonitoring,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ],
          const SizedBox(height: 16),
          _buildSection(
            title: l.callbacksSectionTitle,
            subtitle: l.callbacksSubtitle,
            children: [
              _StatusRow(
                label: l.callbacks,
                isOn: _callbacksActive,
              ),
              const SizedBox(height: 8),
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.surfaceContainerHighest,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text(
                  '${l.lastCallbackEvent}: ${_lastCallbackEvent.isEmpty ? l.noEventsYet : _lastCallbackEvent}',
                  style: Theme.of(context).textTheme.bodySmall,
                ),
              ),
            ],
          ),
          if (_supportsOverlays) ...[
            const SizedBox(height: 16),
            _buildSection(
              title: l.overlaySectionTitle,
              subtitle: l.platformSubtitle,
              children: [
                _StatusRow(
                  label: l.overlay,
                  isOn: _isOverlayImageOn,
                ),
                const SizedBox(height: 12),
                _FeatureButton(
                  label: l.toggleScreenshotWithImage,
                  subtitle: l.overlaySubtitle,
                  onPressed: _toggleScreenshotWithImage,
                ),
              ],
            ),
            const SizedBox(height: 16),
            _buildSection(
              title: l.blurOverlaySectionTitle,
              subtitle: l.platformSubtitle,
              children: [
                _StatusRow(
                  label: l.blurOverlay,
                  isOn: _isOverlayBlurOn,
                ),
                const SizedBox(height: 12),
                _FeatureButton(
                  label: l.toggleScreenshotWithBlur,
                  subtitle: l.blurOverlaySubtitle,
                  onPressed: _toggleScreenshotWithBlur,
                ),
              ],
            ),
            const SizedBox(height: 16),
            _buildSection(
              title: l.colorOverlaySectionTitle,
              subtitle: l.platformSubtitle,
              children: [
                _StatusRow(
                  label: l.colorOverlay,
                  isOn: _isOverlayColorOn,
                ),
                const SizedBox(height: 12),
                _FeatureButton(
                  label: l.toggleScreenshotWithColor,
                  subtitle: l.colorOverlaySubtitle,
                  onPressed: _toggleScreenshotWithColor,
                ),
              ],
            ),
          ],
          if (_supportsProtection) ...[
            const SizedBox(height: 16),
            _buildSection(
              title: l.secureWidgetSectionTitle,
              subtitle: l.secureWidgetSubtitle,
              children: [
                _FeatureButton(
                  label: l.openSecureWidgetDemo,
                  subtitle: l.secureWidgetDemoSubtitle,
                  onPressed: () {
                    Navigator.of(context).push(MaterialPageRoute(
                      builder: (_) => const _SecureWidgetDemoPage(),
                    ));
                  },
                ),
              ],
            ),
            const SizedBox(height: 16),
            _buildSection(
              title: l.perRouteSectionTitle,
              subtitle: l.perRouteSubtitle,
              children: [
                _FeatureButton(
                  label: l.openPerRouteDemo,
                  subtitle: l.perRouteDemoSubtitle,
                  onPressed: () {
                    Navigator.of(context).push(MaterialPageRoute(
                      builder: (_) => const _PerRouteDemoHub(),
                    ));
                  },
                ),
              ],
            ),
          ],
        ],
      ),
    );
  }

  Widget _buildSection({
    required String title,
    required String subtitle,
    required List<Widget> children,
  }) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Wrap(
              runAlignment: WrapAlignment.center,
              children: [
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(width: 8),
                Chip(
                  label: Text(subtitle),
                  visualDensity: VisualDensity.compact,
                ),
              ],
            ),
            const SizedBox(height: 12),
            ...children,
          ],
        ),
      ),
    );
  }

  void _showScreenshotAlert(ScreenshotSnapshot snapshot) {
    final l = AppLocalizations.of(context);
    final details = StringBuffer('${l.path}: ${snapshot.screenshotPath}');
    if (snapshot.timestamp > 0) {
      details.write(
          '\nTime: ${DateTime.fromMillisecondsSinceEpoch(snapshot.timestamp)}');
    }
    if (snapshot.sourceApp.isNotEmpty) {
      details.write('\nSource: ${snapshot.sourceApp}');
    }
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        icon: const Icon(Icons.warning_amber_outlined,
            size: 48, color: Colors.red),
        title: Text(l.screenshotDetected),
        content: Text(details.toString()),
        actions: [
          FilledButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text(l.ok),
          ),
        ],
      ),
    );
  }
}

// ── Reusable Widgets ───────────────────────────────────────────────────

class _FeatureButton extends StatelessWidget {
  const _FeatureButton({
    required this.label,
    required this.subtitle,
    required this.onPressed,
  });

  final String label;
  final String subtitle;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: OutlinedButton(
        onPressed: onPressed,
        style: OutlinedButton.styleFrom(
          padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
          alignment: AlignmentDirectional.centerStart,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(6),
          ),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(label),
            Text(
              subtitle,
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
      ),
    );
  }
}

class _StatusRow extends StatelessWidget {
  const _StatusRow({required this.label, required this.isOn});

  final String label;
  final bool isOn;

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return Row(
      children: [
        Text('$label: ', style: Theme.of(context).textTheme.bodyMedium),
        Container(
          width: 12,
          height: 12,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: isOn ? Colors.green : Colors.grey,
          ),
        ),
        const SizedBox(width: 6),
        Text(isOn ? l.on : l.off),
      ],
    );
  }
}

class _SnapshotInfo extends StatelessWidget {
  const _SnapshotInfo({required this.snapshot});

  final ScreenshotSnapshot snapshot;

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    final style = Theme.of(context).textTheme.bodySmall;
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('${l.protectionActive}: ${snapshot.isScreenshotProtectionOn}',
              style: style?.copyWith(
                  color: snapshot.isScreenshotProtectionOn
                      ? Colors.green
                      : Colors.red)),
          Text('${l.screenshotTaken}: ${snapshot.wasScreenshotTaken}',
              style: style),
          Text('${l.screenRecording}: ${snapshot.isScreenRecording}',
              style: style?.copyWith(
                  color: snapshot.isScreenRecording ? Colors.red : null)),
          Text('${l.path}: ${snapshot.screenshotPath}', style: style),
          if (snapshot.timestamp > 0)
            Text(
                'Timestamp: ${DateTime.fromMillisecondsSinceEpoch(snapshot.timestamp)}',
                style: style),
          if (snapshot.sourceApp.isNotEmpty)
            Text('Source app: ${snapshot.sourceApp}', style: style),
        ],
      ),
    );
  }
}

// ── SecureWidget Demo ─────────────────────────────────────────────────

class _SecureWidgetDemoPage extends StatelessWidget {
  const _SecureWidgetDemoPage();

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return SecureWidget(
      mode: OverlayMode.blur,
      blurRadius: 30.0,
      child: Scaffold(
        appBar: AppBar(title: Text(l.secureWidgetDemoTitle)),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Text(
              l.secureWidgetDemoBody,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ),
        ),
      ),
    );
  }
}

// ── Per-Route Demo ────────────────────────────────────────────────────

final _secureNavigatorObserver = SecureNavigatorObserver(
  policies: {
    '/payment': const SecureRouteConfig(mode: OverlayMode.secure),
    '/profile':
        const SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50.0),
    '/public': const SecureRouteConfig(mode: OverlayMode.none),
  },
  defaultConfig: const SecureRouteConfig(mode: OverlayMode.none),
);

class _PerRouteDemoHub extends StatelessWidget {
  const _PerRouteDemoHub();

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return Navigator(
      observers: [_secureNavigatorObserver],
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case '/payment':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => _DemoRoutePage(
                title: l.paymentPage,
                body: l.paymentPageBody,
                color: Colors.red.shade50,
              ),
            );
          case '/profile':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => _DemoRoutePage(
                title: l.profilePage,
                body: l.profilePageBody,
                color: Colors.blue.shade50,
              ),
            );
          case '/public':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => _DemoRoutePage(
                title: l.publicPage,
                body: l.publicPageBody,
                color: Colors.green.shade50,
              ),
            );
          default:
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => _PerRouteMainPage(),
            );
        }
      },
    );
  }
}

class _PerRouteMainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(l.perRouteSectionTitle),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => Navigator.of(context, rootNavigator: true).pop(),
        ),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _FeatureButton(
            label: l.goToPayment,
            subtitle: 'OverlayMode.secure',
            onPressed: () => Navigator.of(context).pushNamed('/payment'),
          ),
          const SizedBox(height: 8),
          _FeatureButton(
            label: l.goToProfile,
            subtitle: 'OverlayMode.blur (radius: 50)',
            onPressed: () => Navigator.of(context).pushNamed('/profile'),
          ),
          const SizedBox(height: 8),
          _FeatureButton(
            label: l.goToPublic,
            subtitle: 'OverlayMode.none',
            onPressed: () => Navigator.of(context).pushNamed('/public'),
          ),
        ],
      ),
    );
  }
}

class _DemoRoutePage extends StatelessWidget {
  const _DemoRoutePage({
    required this.title,
    required this.body,
    required this.color,
  });

  final String title;
  final String body;
  final Color color;

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return Scaffold(
      backgroundColor: color,
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                body,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              const SizedBox(height: 24),
              FilledButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text(l.back),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
238
likes
160
points
77.3k
downloads

Publisher

verified publisherflutterplaza.com

Weekly Downloads

Flutter plugin to prevent screenshots, detect screen recording, and show blur/color/image overlays in the app switcher on Android, iOS, macOS, Linux, Windows, and Web.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on no_screenshot

Packages that implement no_screenshot