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

A modern, zero-permission Flutter plugin to pick images and videos with OS-level selection limits, native compression, and a rich WhatsApp-style example.

example/lib/main.dart

import 'dart:io';
import 'dart:typed_data';

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(),
      home: const PickerHomePage(),
    );
  }
}

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

  @override
  State<PickerHomePage> createState() => _PickerHomePageState();
}

class _PickerHomePageState extends State<PickerHomePage> {
  List<File> _sentFiles = <File>[];
  List<String> _sentCaptions = <String>[];

  Future<void> _openWhatsAppStylePicker() async {
    final result = await Navigator.of(context).push<PickerResult>(
      MaterialPageRoute(builder: (_) => const WhatsAppStylePickerPage()),
    );

    if (!mounted || result == null) {
      return;
    }

    setState(() {
      _sentFiles = result.files;
      _sentCaptions = result.captions;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('WhatsApp Style Picker Demo'),
        backgroundColor: const Color(0xFF101D25),
      ),
      backgroundColor: const Color(0xFF111B21),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: <Widget>[
              ElevatedButton.icon(
                onPressed: _openWhatsAppStylePicker,
                icon: const Icon(Icons.photo_library),
                label: const Text('Open Media Picker'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF00A884),
                  foregroundColor: Colors.white,
                  minimumSize: const Size(double.infinity, 52),
                ),
              ),
              const SizedBox(height: 12),
              if (_sentCaptions.isNotEmpty)
                Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    'Last captions: ${_sentCaptions.where((text) => text.trim().isNotEmpty).join(' | ')}',
                    style: const TextStyle(color: Colors.white70),
                  ),
                ),
              const SizedBox(height: 12),
              Expanded(
                child: _sentFiles.isEmpty
                    ? const Center(
                        child: Text(
                          'Pick media and tap send',
                          style: TextStyle(color: Colors.white54),
                        ),
                      )
                    : GridView.builder(
                        itemCount: _sentFiles.length,
                        gridDelegate:
                            const SliverGridDelegateWithFixedCrossAxisCount(
                              crossAxisCount: 3,
                              crossAxisSpacing: 6,
                              mainAxisSpacing: 6,
                            ),
                        itemBuilder: (context, index) {
                          return ClipRRect(
                            borderRadius: BorderRadius.circular(6),
                            child: Image.file(
                              _sentFiles[index],
                              fit: BoxFit.cover,
                            ),
                          );
                        },
                      ),
              ),
          ],
        ),
      ),
    );
  }
}

class PickerResult {
  const PickerResult({required this.files, required this.captions});

  final List<File> files;
  final List<String> captions;
}

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

  @override
  State<WhatsAppStylePickerPage> createState() => _WhatsAppStylePickerPageState();
}

class _WhatsAppStylePickerPageState extends State<WhatsAppStylePickerPage> {
  static const int _maxSelection = 30;

  final TextEditingController _captionController = TextEditingController();
  final List<AssetEntity> _selectedAssets = <AssetEntity>[];
  final Map<String, String> _captionsByAssetId = <String, String>{};
  final Map<String, Future<Uint8List?>> _thumbnailFutures =
      <String, Future<Uint8List?>>{};

  List<AssetPathEntity> _albums = <AssetPathEntity>[];
  List<AssetEntity> _assets = <AssetEntity>[];
  AssetPathEntity? _currentAlbum;

  int _tabIndex = 0;
  bool _loading = true;
  String _error = '';

  @override
  void initState() {
    super.initState();
    _loadGallery();
  }

  @override
  void dispose() {
    _captionController.dispose();
    super.dispose();
  }

  Future<void> _loadGallery() async {
    setState(() {
      _loading = true;
      _error = '';
    });

    final PermissionState permission = await PhotoManager.requestPermissionExtend();
    if (!permission.isAuth) {
      if (!mounted) {
        return;
      }
      setState(() {
        _loading = false;
        _error = 'Photo permission denied. Enable it from app settings.';
      });
      return;
    }

    final List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
      hasAll: true,
      type: RequestType.common,
      filterOption: FilterOptionGroup(
        imageOption: const FilterOption(sizeConstraint: SizeConstraint(ignoreSize: true)),
        videoOption: const FilterOption(durationConstraint: DurationConstraint(max: Duration(seconds: 3600))),
      ),
    );

    if (!mounted) {
      return;
    }

    if (albums.isEmpty) {
      setState(() {
        _loading = false;
        _albums = albums;
      });
      return;
    }

    final AssetPathEntity currentAlbum = albums.first;
    final List<AssetEntity> assets = await currentAlbum.getAssetListPaged(page: 0, size: 150);

    if (!mounted) {
      return;
    }

    setState(() {
      _loading = false;
      _albums = albums;
      _currentAlbum = currentAlbum;
      _assets = assets;
    });
  }

  Future<void> _openAlbum(AssetPathEntity album) async {
    setState(() {
      _currentAlbum = album;
      _loading = true;
      _tabIndex = 0;
    });

    final List<AssetEntity> assets = await album.getAssetListPaged(page: 0, size: 150);
    if (!mounted) {
      return;
    }

    setState(() {
      _loading = false;
      _assets = assets;
    });
  }

  void _toggleSelection(AssetEntity entity) {
    setState(() {
      final int index = _selectedAssets.indexOf(entity);
      if (index >= 0) {
        _selectedAssets.removeAt(index);
        _captionsByAssetId.remove(entity.id);
      } else if (_selectedAssets.length < _maxSelection) {
        _selectedAssets.add(entity);
      }
    });
    _syncBottomCaptionField();
  }

  Future<Uint8List?> _thumbnailFuture(AssetEntity entity, int size) {
    return _thumbnailFutures.putIfAbsent(
      '${entity.id}_$size',
      () => entity.thumbnailDataWithSize(ThumbnailSize.square(size)),
    );
  }

  Future<void> _openPreviewEditor() async {
    if (_selectedAssets.isEmpty) {
      return;
    }

    final PreviewEditorResult? result =
        await Navigator.of(context).push<PreviewEditorResult>(
      MaterialPageRoute(
        builder: (_) => PreviewEditorPage(
          selectedAssets: List<AssetEntity>.from(_selectedAssets),
          initialCaptions: Map<String, String>.from(_captionsByAssetId),
          initialIndex: _selectedAssets.length - 1,
          thumbnailProvider: _thumbnailFuture,
        ),
      ),
    );

    if (!mounted || result == null) {
      return;
    }

    setState(() {
      _selectedAssets
        ..clear()
        ..addAll(result.selectedAssets);
      _captionsByAssetId
        ..clear()
        ..addAll(result.captionsByAssetId);
    });
    _syncBottomCaptionField();

    if (result.shouldSend && _selectedAssets.isNotEmpty) {
      await _sendSelection();
    }
  }

  Future<void> _sendSelection() async {
    final List<File> files = <File>[];
    final List<String> captions = <String>[];
    for (final AssetEntity asset in _selectedAssets) {
      final File? file = await asset.file;
      if (file != null) {
        files.add(file);
        captions.add(_captionsByAssetId[asset.id]?.trim() ?? '');
      }
    }

    if (!mounted) {
      return;
    }

    Navigator.of(context).pop(
      PickerResult(files: files, captions: captions),
    );
  }

  AssetEntity? get _activeCaptionAsset =>
      _selectedAssets.isEmpty ? null : _selectedAssets.last;

  void _syncBottomCaptionField() {
    final AssetEntity? asset = _activeCaptionAsset;
    final String nextValue =
        asset == null ? '' : (_captionsByAssetId[asset.id] ?? '');
    if (_captionController.text != nextValue) {
      _captionController.value = TextEditingValue(
        text: nextValue,
        selection: TextSelection.collapsed(offset: nextValue.length),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Column(
          children: <Widget>[
            Container(
              color: const Color(0xFF182229),
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
              child: Row(
                children: <Widget>[
                  TextButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: const Text(
                      'Cancel',
                      style: TextStyle(color: Colors.white, fontSize: 16),
                    ),
                  ),
                  const Spacer(),
                  _TopTabButton(
                    label: 'Photos',
                    selected: _tabIndex == 0,
                    onTap: () => setState(() => _tabIndex = 0),
                  ),
                  const SizedBox(width: 8),
                  _TopTabButton(
                    label: 'Albums',
                    selected: _tabIndex == 1,
                    onTap: () => setState(() => _tabIndex = 1),
                  ),
                  const Spacer(),
                  const CircleAvatar(
                    radius: 14,
                    backgroundColor: Color(0xFF2A3942),
                    child: Text('HD', style: TextStyle(color: Colors.white, fontSize: 10)),
                  ),
                ],
              ),
            ),
            Expanded(child: _buildContent()),
            _buildBottomBar(),
          ],
        ),
      ),
    );
  }

  Widget _buildContent() {
    if (_loading) {
      return const Center(child: CircularProgressIndicator(color: Color(0xFF00A884)));
    }

    if (_error.isNotEmpty) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Text(_error, style: const TextStyle(color: Colors.white70), textAlign: TextAlign.center),
        ),
      );
    }

    if (_tabIndex == 1) {
      return ListView.separated(
        itemCount: _albums.length,
        separatorBuilder: (_, _) => const Divider(height: 1, color: Color(0xFF1F2C33)),
        itemBuilder: (context, index) {
          final AssetPathEntity album = _albums[index];
          final bool selected = _currentAlbum?.id == album.id;

          return ListTile(
            onTap: () => _openAlbum(album),
            tileColor: selected ? const Color(0xFF1A2A31) : Colors.transparent,
            title: Text(album.name, style: const TextStyle(color: Colors.white)),
            subtitle: FutureBuilder<int>(
              future: album.assetCountAsync,
              builder: (context, snapshot) {
                return Text(
                  '${snapshot.data ?? 0} items',
                  style: const TextStyle(color: Colors.white54),
                );
              },
            ),
            trailing: const Icon(Icons.chevron_right, color: Colors.white54),
          );
        },
      );
    }

    return GridView.builder(
      itemCount: _assets.length,
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 4,
        crossAxisSpacing: 2,
        mainAxisSpacing: 2,
      ),
      itemBuilder: (context, index) {
        final AssetEntity entity = _assets[index];
        final int selectedIndex = _selectedAssets.indexOf(entity);
        final bool isSelected = selectedIndex >= 0;

        return GestureDetector(
          key: ValueKey<String>(entity.id),
          onTap: () => _toggleSelection(entity),
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              _AssetThumbnail(
                future: _thumbnailFuture(entity, 300),
              ),
              if (entity.type == AssetType.video)
                const Positioned(
                  left: 6,
                  bottom: 6,
                  child: Icon(Icons.videocam, color: Colors.white, size: 18),
                ),
              Positioned(
                right: 6,
                top: 6,
                child: Container(
                  width: 24,
                  height: 24,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: isSelected ? const Color(0xFF00A884) : Colors.black45,
                    shape: BoxShape.circle,
                    border: Border.all(color: Colors.white70, width: 1),
                  ),
                  child: Text(
                    isSelected ? '${selectedIndex + 1}' : '',
                    style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
                  ),
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildBottomBar() {
    final bool canSend = _selectedAssets.isNotEmpty;

    return Container(
      color: const Color(0xFF1F2C33),
      padding: const EdgeInsets.fromLTRB(12, 8, 12, 10),
      child: Row(
        children: <Widget>[
          GestureDetector(
            onTap: canSend ? _openPreviewEditor : null,
            child: CircleAvatar(
              radius: 20,
              backgroundColor: canSend
                  ? const Color(0xFF2A3942)
                  : const Color(0xFF25343C),
              child: Icon(
                Icons.edit,
                color: canSend ? Colors.white : Colors.white38,
                size: 20,
              ),
            ),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Container(
              height: 42,
              padding: const EdgeInsets.symmetric(horizontal: 12),
              decoration: BoxDecoration(
                color: const Color(0xFF2A3942),
                borderRadius: BorderRadius.circular(21),
              ),
              child: TextField(
                controller: _captionController,
                onChanged: (value) {
                  final AssetEntity? asset = _activeCaptionAsset;
                  if (asset == null) {
                    return;
                  }
                  _captionsByAssetId[asset.id] = value;
                },
                style: const TextStyle(color: Colors.white),
                decoration: InputDecoration(
                  hintText: _activeCaptionAsset == null
                      ? 'Select media to add captions'
                      : 'Add a caption for selected item...',
                  hintStyle: TextStyle(color: Colors.white54),
                  border: InputBorder.none,
                ),
              ),
            ),
          ),
          const SizedBox(width: 10),
          GestureDetector(
            onTap: canSend ? _sendSelection : null,
            child: CircleAvatar(
              radius: 22,
              backgroundColor: canSend ? const Color(0xFF00A884) : const Color(0xFF3A4A53),
              child: Stack(
                clipBehavior: Clip.none,
                children: <Widget>[
                  const Center(
                    child: Text(
                      '➤',
                      style: TextStyle(
                        color: Colors.black,
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                    ),
                  ),
                  if (canSend)
                    Positioned(
                      right: -2,
                      top: -3,
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 5,
                          vertical: 1,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.black,
                          borderRadius: BorderRadius.circular(10),
                        ),
                        child: Text(
                          '${_selectedAssets.length}',
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 10,
                            fontWeight: FontWeight.w700,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _TopTabButton extends StatelessWidget {
  const _TopTabButton({
    required this.label,
    required this.selected,
    required this.onTap,
  });

  final String label;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
        decoration: BoxDecoration(
          color: selected ? Colors.white : const Color(0xFF2A3942),
          borderRadius: BorderRadius.circular(16),
        ),
        child: Text(
          label,
          style: TextStyle(
            color: selected ? Colors.black : Colors.white,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    );
  }
}

class PreviewEditorResult {
  const PreviewEditorResult({
    required this.selectedAssets,
    required this.captionsByAssetId,
    required this.shouldSend,
  });

  final List<AssetEntity> selectedAssets;
  final Map<String, String> captionsByAssetId;
  final bool shouldSend;
}

class PreviewEditorPage extends StatefulWidget {
  const PreviewEditorPage({
    super.key,
    required this.selectedAssets,
    required this.initialCaptions,
    required this.initialIndex,
    required this.thumbnailProvider,
  });

  final List<AssetEntity> selectedAssets;
  final Map<String, String> initialCaptions;
  final int initialIndex;
  final Future<Uint8List?> Function(AssetEntity entity, int size)
      thumbnailProvider;

  @override
  State<PreviewEditorPage> createState() => _PreviewEditorPageState();
}

class _PreviewEditorPageState extends State<PreviewEditorPage> {
  late final TextEditingController _captionController;
  late final PageController _pageController;
  late List<AssetEntity> _selectedAssets;
  late Map<String, String> _captionsByAssetId;
  late int _currentIndex;

  @override
  void initState() {
    super.initState();
    _selectedAssets = List<AssetEntity>.from(widget.selectedAssets);
    _captionsByAssetId = Map<String, String>.from(widget.initialCaptions);
    _currentIndex = widget.initialIndex.clamp(0, _selectedAssets.length - 1);
    _captionController = TextEditingController(
      text: _captionsByAssetId[_selectedAssets[_currentIndex].id] ?? '',
    );
    _pageController = PageController(initialPage: _currentIndex);
  }

  @override
  void dispose() {
    _captionController.dispose();
    _pageController.dispose();
    super.dispose();
  }

  Future<void> _close({required bool shouldSend}) async {
    _saveCurrentCaption();
    Navigator.of(context).pop(
      PreviewEditorResult(
        selectedAssets: _selectedAssets,
        captionsByAssetId: _captionsByAssetId,
        shouldSend: shouldSend,
      ),
    );
  }

  void _removeCurrent() {
    if (_selectedAssets.isEmpty) {
      return;
    }

    _saveCurrentCaption();
    final String removedId = _selectedAssets[_currentIndex].id;
    setState(() {
      _selectedAssets.removeAt(_currentIndex);
      _captionsByAssetId.remove(removedId);
      if (_selectedAssets.isEmpty) {
        Navigator.of(context).pop(
          const PreviewEditorResult(
            selectedAssets: <AssetEntity>[],
            captionsByAssetId: <String, String>{},
            shouldSend: false,
          ),
        );
        return;
      }

      if (_currentIndex >= _selectedAssets.length) {
        _currentIndex = _selectedAssets.length - 1;
      }
      _loadCurrentCaption();
    });

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted && _selectedAssets.isNotEmpty) {
        _pageController.jumpToPage(_currentIndex);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final AssetEntity currentAsset = _selectedAssets[_currentIndex];

    return Scaffold(
      backgroundColor: const Color(0xFF0B0F17),
      body: SafeArea(
        child: Column(
          children: <Widget>[
            Container(
              padding: const EdgeInsets.fromLTRB(10, 8, 10, 6),
              child: Row(
                children: <Widget>[
                  IconButton(
                    onPressed: () => _close(shouldSend: false),
                    icon: const Icon(Icons.arrow_back, color: Colors.white70),
                  ),
                  const SizedBox(width: 10),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        const Text(
                          'Media Preview',
                          style: TextStyle(
                            color: Colors.white70,
                            fontWeight: FontWeight.w700,
                          ),
                        ),
                        Text(
                          currentAsset.title ?? 'media.jpg',
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(
                            color: Colors.white60,
                            fontSize: 12,
                          ),
                        ),
                      ],
                    ),
                  ),
                  IconButton(
                    onPressed: _removeCurrent,
                    icon: const Icon(
                      Icons.delete_outline,
                      color: Color(0xFFE57373),
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(bottom: 6),
              child: Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
                decoration: BoxDecoration(
                  color: const Color(0xFF0F1522),
                  borderRadius: BorderRadius.circular(999),
                  border: Border.all(color: const Color(0xFF2A3942)),
                ),
                child: Text(
                  '${_currentIndex + 1}/${_selectedAssets.length}',
                  style: const TextStyle(color: Colors.white70, fontSize: 12),
                ),
              ),
            ),
            Expanded(
              child: PageView.builder(
                controller: _pageController,
                itemCount: _selectedAssets.length,
                onPageChanged: (value) {
                  _saveCurrentCaption();
                  setState(() {
                    _currentIndex = value;
                    _loadCurrentCaption();
                  });
                },
                itemBuilder: (context, index) {
                  final asset = _selectedAssets[index];
                  final bool isVideo = asset.type == AssetType.video;

                  return FutureBuilder<File?>(
                    future: asset.file,
                    builder: (context, snapshot) {
                      final file = snapshot.data;
                      if (file == null) {
                        return const Center(
                          child: CircularProgressIndicator(
                            color: Color(0xFF00A884),
                          ),
                        );
                      }
                      if (isVideo) {
                        return Center(
                          child: Container(
                            margin: const EdgeInsets.all(24),
                            decoration: BoxDecoration(
                              color: Colors.black,
                              borderRadius: BorderRadius.circular(24),
                            ),
                            child: const AspectRatio(
                              aspectRatio: 9 / 16,
                              child: Center(
                                child: Icon(
                                  Icons.play_circle_fill,
                                  color: Colors.white,
                                  size: 72,
                                ),
                              ),
                            ),
                          ),
                        );
                      }
                      return InteractiveViewer(
                        minScale: 0.8,
                        maxScale: 4,
                        child: Center(
                          child: Image.file(file, fit: BoxFit.contain),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
            Container(
              height: 86,
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              alignment: Alignment.centerLeft,
              child: ListView.separated(
                scrollDirection: Axis.horizontal,
                itemCount: _selectedAssets.length,
                separatorBuilder: (_, _) => const SizedBox(width: 8),
                itemBuilder: (context, index) {
                  final asset = _selectedAssets[index];
                  final isActive = index == _currentIndex;
                  return GestureDetector(
                    onTap: () {
                      _pageController.animateToPage(
                        index,
                        duration: const Duration(milliseconds: 180),
                        curve: Curves.easeOut,
                      );
                    },
                    child: Container(
                      width: 62,
                      height: 62,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(
                          color: isActive
                              ? const Color(0xFF4C8DFF)
                              : Colors.transparent,
                          width: 2,
                        ),
                      ),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(8),
                        child: Stack(
                          fit: StackFit.expand,
                          children: <Widget>[
                            _AssetThumbnail(
                              future: widget.thumbnailProvider(asset, 200),
                            ),
                            Positioned(
                              right: 4,
                              bottom: 4,
                              child: Container(
                                width: 18,
                                height: 18,
                                decoration: const BoxDecoration(
                                  color: Color(0xFF4C8DFF),
                                  shape: BoxShape.circle,
                                ),
                                alignment: Alignment.center,
                                child: Text(
                                  '${index + 1}',
                                  style: const TextStyle(
                                    color: Colors.white,
                                    fontSize: 10,
                                    fontWeight: FontWeight.w700,
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(16, 0, 16, 4),
              child: Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  'Caption for item ${_currentIndex + 1} of ${_selectedAssets.length}',
                  style: const TextStyle(
                    color: Colors.white60,
                    fontSize: 12,
                  ),
                ),
              ),
            ),
            Container(
              padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
              child: Row(
                children: <Widget>[
                  Expanded(
                    child: Container(
                      height: 48,
                      padding: const EdgeInsets.symmetric(horizontal: 16),
                      decoration: BoxDecoration(
                        color: const Color(0xFF0F1522),
                        borderRadius: BorderRadius.circular(24),
                        border: Border.all(color: const Color(0xFF2A3942)),
                      ),
                      child: TextField(
                        controller: _captionController,
                        onChanged: (value) {
                          _captionsByAssetId[currentAsset.id] = value;
                        },
                        style: const TextStyle(color: Colors.white),
                        decoration: const InputDecoration(
                          hintText: 'Add a caption...',
                          border: InputBorder.none,
                          hintStyle: TextStyle(color: Colors.white54),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 10),
                  GestureDetector(
                    onTap: () => _close(shouldSend: true),
                    child: Stack(
                      clipBehavior: Clip.none,
                      children: <Widget>[
                        Container(
                          height: 48,
                          padding: const EdgeInsets.symmetric(
                            horizontal: 18,
                          ),
                          decoration: BoxDecoration(
                            color: const Color(0xFF00A884),
                            borderRadius: BorderRadius.circular(18),
                          ),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: const <Widget>[
                              Icon(
                                Icons.send_rounded,
                                size: 20,
                                color: Colors.white,
                              ),
                              SizedBox(width: 10),
                              Text(
                                'Send',
                                style: TextStyle(
                                  color: Colors.white,
                                  fontWeight: FontWeight.w800,
                                  fontSize: 14,
                                ),
                              ),
                            ],
                          ),
                        ),
                        Positioned(
                          right: 3,
                          top: -6,
                          child: Container(
                            width: 20,
                            height: 20,
                            alignment: Alignment.center,
                            decoration: const BoxDecoration(
                              color: Color(0xFF063B2F),
                              shape: BoxShape.circle,
                            ),
                            child: Text(
                              '${_selectedAssets.length}',
                              style: const TextStyle(
                                color: Colors.white,
                                fontSize: 10,
                                fontWeight: FontWeight.w700,
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _saveCurrentCaption() {
    if (_selectedAssets.isEmpty) {
      return;
    }
    _captionsByAssetId[_selectedAssets[_currentIndex].id] =
        _captionController.text;
  }

  void _loadCurrentCaption() {
    if (_selectedAssets.isEmpty) {
      _captionController.clear();
      return;
    }
    final String value =
        _captionsByAssetId[_selectedAssets[_currentIndex].id] ?? '';
    _captionController.value = TextEditingValue(
      text: value,
      selection: TextSelection.collapsed(offset: value.length),
    );
  }
}

class _AssetThumbnail extends StatelessWidget {
  const _AssetThumbnail({required this.future});

  final Future<Uint8List?> future;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List?>(
      future: future,
      builder: (context, snapshot) {
        if (snapshot.data == null) {
          return const ColoredBox(color: Color(0xFF1F2C33));
        }
        return Image.memory(snapshot.data!, fit: BoxFit.cover);
      },
    );
  }
}
0
likes
150
points
16
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A modern, zero-permission Flutter plugin to pick images and videos with OS-level selection limits, native compression, and a rich WhatsApp-style example.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on image_picker2_plus

Packages that implement image_picker2_plus