copyable_widget 1.0.1 copy "copyable_widget: ^1.0.1" to clipboard
copyable_widget: ^1.0.1 copied to clipboard

Zero-boilerplate clipboard copy for any Flutter widget or text, with haptic feedback and SnackBar confirmation.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'copyable_widget demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF6750A4),
        useMaterial3: true,
        snackBarTheme: const SnackBarThemeData(
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(12)),
          ),
        ),
      ),
      home: const ExamplePage(),
    );
  }
}

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

  @override
  State<ExamplePage> createState() => _ExamplePageState();
}

class _ExamplePageState extends State<ExamplePage> {
  bool _silentCopied = false;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final cs = theme.colorScheme;

    return Scaffold(
      backgroundColor: cs.surface,
      appBar: AppBar(
        title: const Text('copyable_widget'),
        centerTitle: false,
        backgroundColor: cs.surfaceContainerHighest,
        scrolledUnderElevation: 0,
      ),
      body: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
        children: [
          _SectionLabel('1 — Copyable.text (default SnackBar)'),
          const SizedBox(height: 8),
          _DemoCard(
            description:
                'Long-press on mobile, tap on desktop/web. Shows "Copied!" SnackBar.',
            child: Copyable.text(
              'TXN-9182736',
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 18,
                fontWeight: FontWeight.bold,
                color: cs.onSurface,
              ),
            ),
          ),
          const SizedBox(height: 24),

          _SectionLabel('2 — Copyable widget (wraps any child)'),
          const SizedBox(height: 8),
          _DemoCard(
            description:
                'The copy value and the displayed widget are decoupled.',
            child: Copyable(
              value: 'GB29 NWBK 6016 1331 9268 19',
              child: _AccountNumberRow(
                label: 'IBAN',
                number: 'GB29 NWBK 6016 1331 9268 19',
              ),
            ),
          ),
          const SizedBox(height: 24),

          _SectionLabel('3 — Custom SnackBar message'),
          const SizedBox(height: 8),
          _DemoCard(
            description:
                'Pass a custom text to CopyableFeedback.snackBar().',
            child: Copyable.text(
              'PROMO-SAVE20',
              feedback:
                  const CopyableFeedback.snackBar(text: 'Promo code copied!'),
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 18,
                fontWeight: FontWeight.bold,
                letterSpacing: 1.5,
                color: cs.primary,
              ),
            ),
          ),
          const SizedBox(height: 24),

          _SectionLabel('4 — Fully custom feedback'),
          const SizedBox(height: 8),
          _DemoCard(
            description:
                'CopyableFeedback.custom() gives you BuildContext + CopyableEvent.',
            child: Copyable(
              value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
              feedback: CopyableFeedback.custom(
                (context, event) =>
                    ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text(
                      'Copied: ${event.value.substring(0, 8)}… '
                      '(via ${event.mode.name})',
                    ),
                  ),
                ),
              ),
              child: _WalletAddressRow(
                address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
              ),
            ),
          ),
          const SizedBox(height: 24),

          _SectionLabel('5 — Silent copy (no UI feedback)'),
          const SizedBox(height: 8),
          _DemoCard(
            description:
                'CopyableFeedback.none() — app manages its own state indicator.',
            child: Copyable(
              value: 'sk_live_abc123def456',
              feedback: CopyableFeedback.none(),
              mode: CopyableActionMode.tap,
              child: GestureDetector(
                onTap: () => setState(() => _silentCopied = true),
                child: _ApiKeyRow(
                  apiKey: 'sk_live_abc123def456',
                  copied: _silentCopied,
                ),
              ),
            ),
          ),
          const SizedBox(height: 24),

          _SectionLabel('6 — Tap vs Long-press (explicit mode)'),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: _DemoCard(
                  description: 'tap',
                  child: Copyable(
                    value: 'tap-mode',
                    mode: CopyableActionMode.tap,
                    feedback: const CopyableFeedback.snackBar(
                      text: 'Tap mode: copied!',
                    ),
                    child: Center(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(Icons.touch_app_rounded,
                              color: cs.primary, size: 32),
                          const SizedBox(height: 4),
                          Text(
                            'Tap me',
                            style: TextStyle(color: cs.primary),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: _DemoCard(
                  description: 'longPress',
                  child: Copyable(
                    value: 'long-press-mode',
                    mode: CopyableActionMode.longPress,
                    feedback: const CopyableFeedback.snackBar(
                      text: 'Long-press mode: copied!',
                    ),
                    child: Center(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(Icons.pan_tool_rounded,
                              color: cs.secondary, size: 32),
                          const SizedBox(height: 4),
                          Text(
                            'Hold me',
                            style: TextStyle(color: cs.secondary),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: 40),
        ],
      ),
    );
  }
}

// ── Supporting widgets ────────────────────────────────────────────────────────

class _SectionLabel extends StatelessWidget {
  const _SectionLabel(this.text);
  final String text;

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: Theme.of(context).textTheme.labelLarge?.copyWith(
            color: Theme.of(context).colorScheme.outline,
          ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  const _DemoCard({required this.description, required this.child});
  final String description;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: cs.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            child,
            const SizedBox(height: 12),
            Text(
              description,
              style: Theme.of(context)
                  .textTheme
                  .bodySmall
                  ?.copyWith(color: cs.outline),
            ),
          ],
        ),
      ),
    );
  }
}

class _AccountNumberRow extends StatelessWidget {
  const _AccountNumberRow({required this.label, required this.number});
  final String label;
  final String number;

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Row(
      children: [
        Icon(Icons.account_balance_rounded, color: cs.primary, size: 20),
        const SizedBox(width: 12),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(label,
                style: Theme.of(context)
                    .textTheme
                    .labelSmall
                    ?.copyWith(color: cs.outline)),
            Text(number,
                style: TextStyle(
                  fontFamily: 'monospace',
                  fontWeight: FontWeight.w600,
                  color: cs.onSurface,
                )),
          ],
        ),
        const Spacer(),
        Icon(Icons.copy_rounded, color: cs.outline, size: 18),
      ],
    );
  }
}

class _WalletAddressRow extends StatelessWidget {
  const _WalletAddressRow({required this.address});
  final String address;

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final short =
        '${address.substring(0, 8)}…${address.substring(address.length - 6)}';
    return Row(
      children: [
        Container(
          width: 36,
          height: 36,
          decoration: BoxDecoration(
            color: cs.secondaryContainer,
            shape: BoxShape.circle,
          ),
          child: Icon(Icons.currency_bitcoin_rounded,
              color: cs.secondary, size: 20),
        ),
        const SizedBox(width: 12),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Wallet address',
                style: Theme.of(context)
                    .textTheme
                    .labelSmall
                    ?.copyWith(color: cs.outline)),
            Text(short,
                style: TextStyle(
                  fontFamily: 'monospace',
                  fontWeight: FontWeight.w600,
                  color: cs.onSurface,
                )),
          ],
        ),
        const Spacer(),
        Icon(Icons.copy_rounded, color: cs.outline, size: 18),
      ],
    );
  }
}

class _ApiKeyRow extends StatelessWidget {
  const _ApiKeyRow({required this.apiKey, required this.copied});
  final String apiKey;
  final bool copied;

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Row(
      children: [
        Icon(Icons.key_rounded, color: cs.tertiary, size: 20),
        const SizedBox(width: 12),
        Expanded(
          child: Text(
            apiKey,
            style: TextStyle(
              fontFamily: 'monospace',
              color: cs.onSurface,
            ),
            overflow: TextOverflow.ellipsis,
          ),
        ),
        const SizedBox(width: 8),
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 200),
          child: copied
              ? Icon(Icons.check_circle_rounded,
                  key: const ValueKey('check'), color: Colors.green, size: 20)
              : Icon(Icons.copy_rounded,
                  key: const ValueKey('copy'), color: cs.outline, size: 18),
        ),
      ],
    );
  }
}
0
likes
0
points
241
downloads

Publisher

unverified uploader

Weekly Downloads

Zero-boilerplate clipboard copy for any Flutter widget or text, with haptic feedback and SnackBar confirmation.

Repository (GitHub)
View/report issues

Topics

#clipboard #clipboard-widget #copy #copyable #copyable-widget

License

unknown (license)

Dependencies

flutter

More

Packages that depend on copyable_widget