hq_picker 0.0.3 copy "hq_picker: ^0.0.3" to clipboard
hq_picker: ^0.0.3 copied to clipboard

A stateful widget that allows users to pick media files (images, videos, audio, files) from their device.

example/lib/main.dart

import 'dart:io';

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HQPicker Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6C63FF),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFF0F0F1A),
      ),
      home: const ExampleHomePage(),
    );
  }
}

// ─────────────────────────────────────────────
// Data model for a use-case card
// ─────────────────────────────────────────────
class _UseCase {
  final String title;
  final String subtitle;
  final IconData icon;
  final Color color;
  final Future<List<HQPickerResult>> Function(BuildContext) action;

  const _UseCase({
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.color,
    required this.action,
  });
}

// ─────────────────────────────────────────────
// Home Page
// ─────────────────────────────────────────────
class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key});

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  List<HQPickerResult> _results = [];
  List<AssetEntity> _telegramAssets = [];
  List<FileSystemEntity> _telegramFiles = [];

  final GlobalKey<HQPickerTelegramMediaPickersState> _inlineTelegramKey =
      GlobalKey();

  // ── Tab 1: Shapes ───────────────────────────
  late final List<_UseCase> _shapeCases;

  // ── Tab 2: Types ────────────────────────────
  late final List<_UseCase> _typeCases;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);

    _shapeCases = [
      _UseCase(
        title: 'Instagram',
        subtitle: 'Full-screen preview + grid • all media',
        icon: Icons.photo_camera,
        color: const Color(0xFFE1306C),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.instagram,
          maxCount: 5,
          requestType: HQPickerRequestType.all,
          config: const HQPickerConfig(
            enableCropping: true,
            compressImage: true,
          ),
        ),
      ),
      _UseCase(
        title: 'Custom (TabBar)',
        subtitle: 'Images & Videos tabs • multi-select',
        icon: Icons.grid_view,
        color: const Color(0xFF6C63FF),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.custom,
          maxCount: 5,
          requestType: HQPickerRequestType.all,
        ),
      ),
      _UseCase(
        title: 'Bottom Sheet',
        subtitle: 'Standard modal bottom sheet • images',
        icon: Icons.vertical_align_bottom,
        color: const Color(0xFF00BFA5),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.bottomSheet,
          maxCount: 5,
          requestType: HQPickerRequestType.image,
        ),
      ),
      _UseCase(
        title: 'Scaffold Bottom Sheet',
        subtitle: 'Full-height modal • videos',
        icon: Icons.expand_less,
        color: const Color(0xFFFFA000),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.scaffoldBottomSheet,
          maxCount: 5,
          requestType: HQPickerRequestType.video,
        ),
      ),
      _UseCase(
        title: 'Bottom Sheet Image Selector',
        subtitle: 'Album dropdown + modal • images',
        icon: Icons.photo_library,
        color: const Color(0xFF26A69A),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.bottomSheetImageSelector,
          maxCount: 5,
          requestType: HQPickerRequestType.image,
        ),
      ),
      _UseCase(
        title: 'Telegram',
        subtitle: 'Sliding sheet • camera + gallery + audio + files',
        icon: Icons.send,
        color: const Color(0xFF229ED9),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.telegram,
          maxCount: 10,
          requestType: HQPickerRequestType.all,
        ),
      ),
      _UseCase(
        title: 'Document Picker',
        subtitle: 'Native file picker • PDF / DOC / etc.',
        icon: Icons.description,
        color: const Color(0xFFEF5350),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.document,
          maxCount: 5,
          allowedExtensions: ['pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx'],
        ),
      ),
      _UseCase(
        title: 'Directory Picker',
        subtitle: 'Native folder picker',
        icon: Icons.folder_open,
        color: const Color(0xFFFF7043),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.directory,
          maxCount: 1,
        ),
      ),
    ];

    _typeCases = [
      _UseCase(
        title: 'Pick Image',
        subtitle: 'Instagram shape • images only • with crop',
        icon: Icons.image,
        color: const Color(0xFFE1306C),
        action: (ctx) => HQPicker.pickImage(
          context: ctx,
          shape: HQPickerShape.instagram,
          maxCount: 5,
          config: const HQPickerConfig(enableCropping: true),
        ),
      ),
      _UseCase(
        title: 'Pick Video',
        subtitle: 'Custom shape • videos only',
        icon: Icons.videocam,
        color: const Color(0xFF6C63FF),
        action: (ctx) => HQPicker.pickVideo(
          context: ctx,
          shape: HQPickerShape.custom,
          maxCount: 3,
        ),
      ),
      _UseCase(
        title: 'Pick Images — Bottom Sheet',
        subtitle: 'bottomSheet shape • images only',
        icon: Icons.photo,
        color: const Color(0xFF00BFA5),
        action: (ctx) => HQPicker.pickImage(
          context: ctx,
          shape: HQPickerShape.bottomSheet,
          maxCount: 5,
        ),
      ),
      _UseCase(
        title: 'Pick Videos — Scaffold Sheet',
        subtitle: 'scaffoldBottomSheet • videos only',
        icon: Icons.video_library,
        color: const Color(0xFFFFA000),
        action: (ctx) => HQPicker.pickVideo(
          context: ctx,
          shape: HQPickerShape.scaffoldBottomSheet,
          maxCount: 5,
        ),
      ),
      _UseCase(
        title: 'Pick All Media — Telegram',
        subtitle: 'Telegram shape • images + videos',
        icon: Icons.perm_media,
        color: const Color(0xFF229ED9),
        action: (ctx) => HQPicker.pick(
          context: ctx,
          shape: HQPickerShape.telegram,
          maxCount: 10,
          requestType: HQPickerRequestType.all,
        ),
      ),
      _UseCase(
        title: 'Pick Document',
        subtitle: 'Native picker • any file type',
        icon: Icons.attach_file,
        color: const Color(0xFFEF5350),
        action: (ctx) => HQPicker.pickDocument(context: ctx, maxCount: 5),
      ),
      _UseCase(
        title: 'Pick Directory',
        subtitle: 'Native picker • folder/directory',
        icon: Icons.folder,
        color: const Color(0xFFFF7043),
        action: (ctx) => HQPicker.pickDirectory(context: ctx),
      ),
      _UseCase(
        title: 'Pick Images + Compress',
        subtitle: 'Instagram • images • compress 60%',
        icon: Icons.compress,
        color: const Color(0xFF26A69A),
        action: (ctx) => HQPicker.pickImage(
          context: ctx,
          shape: HQPickerShape.instagram,
          maxCount: 5,
          config: const HQPickerConfig(
            compressImage: true,
            compressQuality: 60,
          ),
        ),
      ),
    ];
  }

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

  Future<void> _run(_UseCase useCase) async {
    try {
      final results = await useCase.action(context);
      if (!mounted) return;
      setState(() => _results = results);
      if (results.isNotEmpty) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('✅ ${results.length} item(s) picked'),
            backgroundColor: const Color(0xFF1E1E2E),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('❌ Error: $e'),
          backgroundColor: Colors.red.shade900,
          behavior: SnackBarBehavior.floating,
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F0F1A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1A1A2E),
        title: const Text(
          'HQPicker Showcase',
          style: TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.bold,
            fontSize: 20,
          ),
        ),
        bottom: TabBar(
          controller: _tabController,
          indicatorColor: const Color(0xFF6C63FF),
          labelColor: const Color(0xFF6C63FF),
          unselectedLabelColor: Colors.white54,
          tabs: const [
            Tab(icon: Icon(Icons.style), text: 'Shapes'),
            Tab(icon: Icon(Icons.category), text: 'Types'),
            Tab(icon: Icon(Icons.telegram), text: 'Telegram'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          // ── Tab 1: All Shapes ──────────────────
          _UseCaseGrid(cases: _shapeCases, onTap: _run, results: _results),

          // ── Tab 2: All Types / Methods ─────────
          _UseCaseGrid(cases: _typeCases, onTap: _run, results: _results),

          // ── Tab 3: Inline Telegram Sheet ───────
          _InlineTelegramTab(
            telegramKey: _inlineTelegramKey,
            assets: _telegramAssets,
            files: _telegramFiles,
            onPicked: (assets, files) {
              setState(() {
                _telegramAssets = assets ?? [];
                _telegramFiles = files?.cast<FileSystemEntity>() ?? [];
              });
            },
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────
// Grid of use-case cards + results panel
// ─────────────────────────────────────────────
class _UseCaseGrid extends StatelessWidget {
  final List<_UseCase> cases;
  final Future<void> Function(_UseCase) onTap;
  final List<HQPickerResult> results;

  const _UseCaseGrid({
    required this.cases,
    required this.onTap,
    required this.results,
  });

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverPadding(
          padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
          sliver: SliverGrid(
            delegate: SliverChildBuilderDelegate(
              (ctx, i) =>
                  _CaseCard(useCase: cases[i], onTap: () => onTap(cases[i])),
              childCount: cases.length,
            ),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
              childAspectRatio: 1.35,
            ),
          ),
        ),
        if (results.isNotEmpty)
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Divider(color: Colors.white24),
                  Text(
                    '${results.length} Result(s)',
                    style: const TextStyle(
                      color: Colors.white70,
                      fontWeight: FontWeight.bold,
                      fontSize: 14,
                    ),
                  ),
                  const SizedBox(height: 8),
                ],
              ),
            ),
          ),
        if (results.isNotEmpty)
          SliverPadding(
            padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
            sliver: SliverList(
              delegate: SliverChildBuilderDelegate(
                (ctx, i) => _ResultTile(result: results[i]),
                childCount: results.length,
              ),
            ),
          ),
      ],
    );
  }
}

// ─────────────────────────────────────────────
// Single use-case card
// ─────────────────────────────────────────────
class _CaseCard extends StatelessWidget {
  final _UseCase useCase;
  final VoidCallback onTap;

  const _CaseCard({required this.useCase, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(16),
          gradient: LinearGradient(
            colors: [
              useCase.color.withValues(alpha: 0.85),
              useCase.color.withValues(alpha: 0.45),
            ],
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
          ),
          boxShadow: [
            BoxShadow(
              color: useCase.color.withValues(alpha: 0.3),
              blurRadius: 12,
              offset: const Offset(0, 4),
            ),
          ],
        ),
        padding: const EdgeInsets.all(14),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(useCase.icon, color: Colors.white, size: 28),
            const Spacer(),
            Text(
              useCase.title,
              style: const TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
                fontSize: 13,
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            const SizedBox(height: 3),
            Text(
              useCase.subtitle,
              style: TextStyle(
                color: Colors.white.withValues(alpha: 0.75),
                fontSize: 10.5,
              ),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────
// Single result row
// ─────────────────────────────────────────────
class _ResultTile extends StatelessWidget {
  final HQPickerResult result;
  const _ResultTile({required this.result});

  @override
  Widget build(BuildContext context) {
    final file = result.file;
    final asset = result.asset;
    final isImage =
        asset?.type == AssetType.image ||
        (file != null &&
            [
              '.jpg',
              '.jpeg',
              '.png',
              '.gif',
              '.webp',
            ].any((ext) => file.path.toLowerCase().endsWith(ext)));

    return Container(
      margin: const EdgeInsets.only(bottom: 8),
      decoration: BoxDecoration(
        color: const Color(0xFF1E1E2E),
        borderRadius: BorderRadius.circular(12),
      ),
      child: ListTile(
        leading: ClipRRect(
          borderRadius: BorderRadius.circular(8),
          child: SizedBox(
            width: 50,
            height: 50,
            child: file != null && isImage
                ? Image.file(
                    file,
                    fit: BoxFit.cover,
                    errorBuilder: (_, __, ___) => _typeIcon(asset),
                  )
                : _typeIcon(asset),
          ),
        ),
        title: Text(
          file != null ? file.path.split('/').last : 'Asset ${asset?.id ?? ''}',
          style: const TextStyle(color: Colors.white, fontSize: 13),
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
        subtitle: Text(
          asset != null ? 'Type: ${asset.type.name}' : file?.path ?? '',
          style: const TextStyle(color: Colors.white54, fontSize: 11),
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
      ),
    );
  }

  Widget _typeIcon(AssetEntity? asset) {
    IconData icon;
    Color color;
    switch (asset?.type) {
      case AssetType.video:
        icon = Icons.videocam;
        color = const Color(0xFF6C63FF);
        break;
      case AssetType.audio:
        icon = Icons.audiotrack;
        color = const Color(0xFFFFA000);
        break;
      default:
        icon = Icons.insert_drive_file;
        color = Colors.white38;
    }
    return Container(
      color: color.withValues(alpha: 0.15),
      child: Icon(icon, color: color, size: 26),
    );
  }
}

// ─────────────────────────────────────────────
// Tab 3 — Inline Telegram sheet
// ─────────────────────────────────────────────
class _InlineTelegramTab extends StatelessWidget {
  final GlobalKey<HQPickerTelegramMediaPickersState> telegramKey;
  final List<AssetEntity> assets;
  final List<FileSystemEntity> files;
  final void Function(List<AssetEntity>?, List<dynamic>?) onPicked;

  const _InlineTelegramTab({
    required this.telegramKey,
    required this.assets,
    required this.files,
    required this.onPicked,
  });

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Header card
          Container(
            decoration: BoxDecoration(
              gradient: const LinearGradient(
                colors: [Color(0xFF229ED9), Color(0xFF1565C0)],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
              borderRadius: BorderRadius.circular(16),
            ),
            padding: const EdgeInsets.all(20),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Icon(Icons.telegram, color: Colors.white, size: 36),
                const SizedBox(height: 10),
                const Text(
                  'Inline Telegram Picker',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  'Sliding sheet embedded in the page.\nSupports camera, gallery, audio & files.',
                  style: TextStyle(
                    color: Colors.white.withValues(alpha: 0.8),
                    fontSize: 13,
                  ),
                ),
                const SizedBox(height: 14),
                ElevatedButton.icon(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.white,
                    foregroundColor: const Color(0xFF229ED9),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),
                  ),
                  icon: const Icon(Icons.open_in_new),
                  label: const Text('Toggle Telegram Sheet'),
                  onPressed: () =>
                      telegramKey.currentState?.toggleSheet(context),
                ),
              ],
            ),
          ),

          const SizedBox(height: 16),

          // Inline widget
          HQPickerTelegramMediaPickers(
            key: telegramKey,
            requestType: HQPickerRequestType.all,
            maxCountPickMedia: 10,
            maxCountPickFiles: 10,
            primeryColor: const Color(0xFF229ED9),
            isRealCameraView: false,
            onMediaPicked: onPicked,
          ),

          // Picked assets
          if (assets.isNotEmpty) ...[
            const Divider(color: Colors.white24),
            Text(
              '${assets.length} Media Asset(s)',
              style: const TextStyle(
                color: Colors.white70,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            ...assets.map(
              (a) => Container(
                margin: const EdgeInsets.only(bottom: 6),
                decoration: BoxDecoration(
                  color: const Color(0xFF1E1E2E),
                  borderRadius: BorderRadius.circular(10),
                ),
                child: ListTile(
                  leading: Icon(
                    a.type == AssetType.video ? Icons.videocam : Icons.image,
                    color: const Color(0xFF229ED9),
                  ),
                  title: Text(
                    'Asset: ${a.id}',
                    style: const TextStyle(color: Colors.white, fontSize: 13),
                  ),
                  subtitle: Text(
                    'Type: ${a.type.name}',
                    style: const TextStyle(color: Colors.white54, fontSize: 11),
                  ),
                ),
              ),
            ),
          ],

          // Picked files
          if (files.isNotEmpty) ...[
            const Divider(color: Colors.white24),
            Text(
              '${files.length} File(s)',
              style: const TextStyle(
                color: Colors.white70,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            ...files.map(
              (f) => Container(
                margin: const EdgeInsets.only(bottom: 6),
                decoration: BoxDecoration(
                  color: const Color(0xFF1E1E2E),
                  borderRadius: BorderRadius.circular(10),
                ),
                child: ListTile(
                  leading: const Icon(
                    Icons.insert_drive_file,
                    color: Color(0xFFFFA000),
                  ),
                  title: Text(
                    f.path.split('/').last,
                    style: const TextStyle(color: Colors.white, fontSize: 13),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  subtitle: Text(
                    f.path,
                    style: const TextStyle(color: Colors.white54, fontSize: 10),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ),
            ),
          ],
        ],
      ),
    );
  }
}