unified_file_picker 0.1.2 copy "unified_file_picker: ^0.1.2" to clipboard
unified_file_picker: ^0.1.2 copied to clipboard

Cross-platform Flutter file and folder picker with a unified, fully customizable explorer-style UI — list, details, tiles, and icons views, search, thumbnails, theming, and RTL on Android, iOS, and desktop.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:unified_file_picker/unified_file_picker.dart';

import 'demo_config.dart';
import 'live_preview_pane.dart';

/// Live customization studio for **unified_file_picker** by quds.cc.
///
/// Run from the `example/` directory (or use the workspace launch config).
void main() {
  runApp(const QudsFilePickerExampleApp());
}

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

  @override
  State<QudsFilePickerExampleApp> createState() =>
      _QudsFilePickerExampleAppState();
}

class _QudsFilePickerExampleAppState extends State<QudsFilePickerExampleApp> {
  Locale _locale = const Locale('en');
  TextDirection _textDirection = TextDirection.ltr;

  void _toggleLocale() {
    setState(() {
      if (_locale.languageCode == 'en') {
        _locale = const Locale('ar');
        _textDirection = TextDirection.rtl;
      } else {
        _locale = const Locale('en');
        _textDirection = TextDirection.ltr;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Unified File Picker — quds.cc',
      locale: _locale,
      localizationsDelegates: const [
        UnifiedFilePickerLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [
        Locale('en'),
        Locale('ar'),
      ],
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF5B4FCF),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF5B4FCF),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: ExampleHomePage(
        textDirection: _textDirection,
        onToggleLocale: _toggleLocale,
        localeLabel: _locale.languageCode == 'ar' ? 'English' : 'العربية',
      ),
    );
  }
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({
    super.key,
    required this.textDirection,
    required this.onToggleLocale,
    required this.localeLabel,
  });

  final TextDirection textDirection;
  final VoidCallback onToggleLocale;
  final String localeLabel;

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

class _ExampleHomePageState extends State<ExampleHomePage> {
  DemoConfig _config = DemoConfig.defaults();
  List<PickedFile> _selectedItems = const [];
  String _status = 'Adjust the settings on the left — the picker updates live on the right.';
  bool _showLivePreview = true;
  double _previewWidth = 560;
  FilePickerMode _previewMode = FilePickerMode.open;

  static const double _minPreviewWidth = 360;
  static const double _minSettingsWidth = 420;
  static const double _splitBreakpoint = 960;

  ColorScheme get _scheme => ColorScheme.fromSeed(
        seedColor: _config.seedColor,
        brightness: _config.isDark ? Brightness.dark : Brightness.light,
      );

  UnifiedFilePickerStrings? get _strings =>
      _config.useBrandStrings ? const BrandFilePickerStrings() : null;

  FilePickerOptions _previewOptions() {
    return _config
        .options(
          mode: _previewMode,
          type: FilePickerType.any,
          selection: SelectionMode.multiple,
          suggestedFileName: 'quds-notes',
          strings: _strings,
          textDirection: widget.textDirection,
        )
        .copyWith(
          theme: _config.theme(_scheme),
          presentation: FilePickerPresentation.fullscreen,
        );
  }

  FilePickerOptions _themedOptions(FilePickerOptions options) {
    return options.copyWith(
      theme: _config.theme(_scheme),
      strings: _strings ?? options.strings,
      textDirection: widget.textDirection,
    );
  }

  Future<void> _launchPicker(FilePickerOptions options) async {
    final result = await UnifiedFilePicker.pick(
      context,
      options: _themedOptions(options),
    );
    if (!mounted || result == null) {
      return;
    }

    setState(() {
      _selectedItems = result;
      _status = 'Selected ${result.length} item(s)';
    });
  }

  Future<void> _pickFiles(FilePickerType type) {
    return _launchPicker(
      _config.options(
        mode: FilePickerMode.open,
        type: type,
        selection: type == FilePickerType.documents
            ? SelectionMode.single
            : SelectionMode.multiple,
        strings: _strings,
        textDirection: widget.textDirection,
      ),
    );
  }

  Future<void> _pickCustomFiles() {
    return _launchPicker(
      _config.options(
        mode: FilePickerMode.open,
        selection: SelectionMode.multiple,
        customFilters: const [
          FileTypeFilter(label: 'Markdown', extensions: ['.md', '.markdown']),
          FileTypeFilter(label: 'Code', extensions: ['.dart', '.js', '.py']),
        ],
        strings: _strings,
        textDirection: widget.textDirection,
      ),
    );
  }

  Future<void> _pickFolder() {
    return _launchPicker(
      _config.options(
        mode: FilePickerMode.folder,
        selection: SelectionMode.single,
        strings: _strings,
        textDirection: widget.textDirection,
      ),
    );
  }

  Future<void> _saveFile() async {
    final options = _config.options(
      mode: FilePickerMode.save,
      suggestedFileName: 'quds-notes',
      customFilters: const [
        FileTypeFilter(label: 'Text', extensions: ['.txt']),
        FileTypeFilter(label: 'Markdown', extensions: ['.md']),
      ],
      strings: _strings,
      textDirection: widget.textDirection,
    );

    final themed = _themedOptions(options);

    final location = await UnifiedFilePicker.pick(context, options: themed);
    if (!mounted || location == null || location.isEmpty) {
      return;
    }

    final saved = await UnifiedFilePicker.writeBytes(
      location.first,
      'Saved from the quds.cc unified_file_picker example.\n'.codeUnits,
    );

    setState(() {
      _selectedItems = location;
      _status = saved
          ? 'Saved to ${location.first.path}'
          : 'Save path chosen, but writing failed.';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: widget.textDirection,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('unified_file_picker'),
          actions: [
            TextButton(
              onPressed: widget.onToggleLocale,
              child: Text(widget.localeLabel),
            ),
          ],
        ),
        body: LayoutBuilder(
          builder: (context, constraints) {
            final canSplit =
                constraints.maxWidth >= _splitBreakpoint && _showLivePreview;
            final maxPreviewWidth =
                constraints.maxWidth - _minSettingsWidth - 10;

            return Row(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Expanded(
                  child: ListView(
                    padding: const EdgeInsets.all(16),
                    children: [
                      _PublisherBanner(scheme: _scheme),
                      const SizedBox(height: 16),
                      _CustomizationPanel(
                        config: _config,
                        scheme: _scheme,
                        showLivePreview: _showLivePreview,
                        onChanged: (value) => setState(() => _config = value),
                        onReset: () =>
                            setState(() => _config = DemoConfig.defaults()),
                        onShowLivePreviewChanged: (value) =>
                            setState(() => _showLivePreview = value),
                      ),
                      const SizedBox(height: 24),
                      Text(
                        'Launch picker (modal)',
                        style: Theme.of(context).textTheme.titleMedium,
                      ),
                      const SizedBox(height: 8),
                      Wrap(
                        spacing: 12,
                        runSpacing: 12,
                        children: [
                          for (final type in FilePickerType.values)
                            FilledButton.icon(
                              onPressed: () => _pickFiles(type),
                              icon: Icon(_iconForType(type)),
                              label: Text(type.label),
                            ),
                          FilledButton.icon(
                            onPressed: _pickCustomFiles,
                            icon: const Icon(Icons.tune_rounded),
                            label: const Text('Custom filters'),
                          ),
                          FilledButton.icon(
                            onPressed: _pickFolder,
                            icon: const Icon(Icons.folder_rounded),
                            label: const Text('Pick folder'),
                          ),
                          FilledButton.icon(
                            onPressed: _saveFile,
                            icon: const Icon(Icons.save_rounded),
                            label: const Text('Save file'),
                          ),
                        ],
                      ),
                      const SizedBox(height: 24),
                      Text(_status, style: Theme.of(context).textTheme.titleMedium),
                      const SizedBox(height: 8),
                      if (_selectedItems.isEmpty)
                        Text(
                          'Picker results appear here.',
                          style:
                              Theme.of(context).textTheme.bodyMedium?.copyWith(
                                    color: Theme.of(context)
                                        .colorScheme
                                        .onSurfaceVariant,
                                  ),
                        )
                      else
                        ..._selectedItems.map(
                          (item) => Card(
                            child: ListTile(
                              leading: Icon(
                                item.isDirectory
                                    ? Icons.folder_rounded
                                    : Icons.insert_drive_file_rounded,
                              ),
                              title: Text(item.name),
                              subtitle: Text(
                                item.path,
                                maxLines: 3,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ),
                          ),
                        ),
                    ],
                  ),
                ),
                if (canSplit) ...[
                  PreviewResizeHandle(
                    textDirection: widget.textDirection,
                    onDrag: (expandDelta) {
                      setState(() {
                        _previewWidth = (_previewWidth + expandDelta).clamp(
                          _minPreviewWidth,
                          maxPreviewWidth,
                        );
                      });
                    },
                  ),
                  SizedBox(
                    width: _previewWidth.clamp(
                      _minPreviewWidth,
                      maxPreviewWidth,
                    ),
                    child: LivePreviewPane(
                      options: _previewOptions(),
                      scheme: _scheme,
                      isDark: _config.isDark,
                      mode: _previewMode,
                      onModeChanged: (mode) =>
                          setState(() => _previewMode = mode),
                      onPicked: (items) => setState(() {
                        _selectedItems = items;
                        _status =
                            'Preview selected ${items.length} item(s)';
                      }),
                      onReload: () => setState(
                        () => _status = 'Preview reloaded.',
                      ),
                    ),
                  ),
                ],
              ],
            );
          },
        ),
      ),
    );
  }

  IconData _iconForType(FilePickerType type) {
    switch (type) {
      case FilePickerType.any:
        return Icons.folder_open_rounded;
      case FilePickerType.documents:
        return Icons.description_rounded;
      case FilePickerType.images:
        return Icons.image_rounded;
      case FilePickerType.videos:
        return Icons.videocam_rounded;
    }
  }
}

class _PublisherBanner extends StatelessWidget {
  const _PublisherBanner({required this.scheme});

  final ColorScheme scheme;

  @override
  Widget build(BuildContext context) {
    return Card(
      color: scheme.primaryContainer,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Fully customizable file picker',
              style: Theme.of(context).textTheme.titleLarge?.copyWith(
                    color: scheme.onPrimaryContainer,
                    fontWeight: FontWeight.w700,
                  ),
            ),
            const SizedBox(height: 8),
            Text(
              'Published by quds.cc. Tune colors, layout, transitions, '
              'strings, icons, and behavior live — the side preview '
              'updates instantly as you change settings.',
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    color: scheme.onPrimaryContainer,
                  ),
            ),
          ],
        ),
      ),
    );
  }
}

class _CustomizationPanel extends StatefulWidget {
  const _CustomizationPanel({
    required this.config,
    required this.scheme,
    required this.showLivePreview,
    required this.onChanged,
    required this.onReset,
    required this.onShowLivePreviewChanged,
  });

  final DemoConfig config;
  final ColorScheme scheme;
  final bool showLivePreview;
  final ValueChanged<DemoConfig> onChanged;
  final VoidCallback onReset;
  final ValueChanged<bool> onShowLivePreviewChanged;

  @override
  State<_CustomizationPanel> createState() => _CustomizationPanelState();
}

class _CustomizationPanelState extends State<_CustomizationPanel> {
  late final TextEditingController _titleController;
  late final TextEditingController _confirmController;
  late final TextEditingController _cancelController;

  @override
  void initState() {
    super.initState();
    _titleController = TextEditingController(text: widget.config.customTitle);
    _confirmController =
        TextEditingController(text: widget.config.customConfirmLabel);
    _cancelController =
        TextEditingController(text: widget.config.customCancelLabel);
  }

  @override
  void didUpdateWidget(covariant _CustomizationPanel oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.config.customTitle != widget.config.customTitle &&
        _titleController.text != widget.config.customTitle) {
      _titleController.text = widget.config.customTitle;
    }
    if (oldWidget.config.customConfirmLabel != widget.config.customConfirmLabel &&
        _confirmController.text != widget.config.customConfirmLabel) {
      _confirmController.text = widget.config.customConfirmLabel;
    }
    if (oldWidget.config.customCancelLabel != widget.config.customCancelLabel &&
        _cancelController.text != widget.config.customCancelLabel) {
      _cancelController.text = widget.config.customCancelLabel;
    }
  }

  @override
  void dispose() {
    _titleController.dispose();
    _confirmController.dispose();
    _cancelController.dispose();
    super.dispose();
  }

  void _reset() {
    widget.onReset();
    _titleController.text = DemoConfig.defaults().customTitle;
    _confirmController.text = DemoConfig.defaults().customConfirmLabel;
    _cancelController.text = DemoConfig.defaults().customCancelLabel;
  }

  @override
  Widget build(BuildContext context) {
    final config = widget.config;
    final onChanged = widget.onChanged;
    final scheme = widget.scheme;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Text(
                  'Customization',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const Spacer(),
                TextButton(onPressed: _reset, child: const Text('Reset')),
              ],
            ),
            const SizedBox(height: 12),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Live side preview'),
              subtitle: const Text(
                'Shows the picker beside settings (wide windows). '
                'Drag the divider to resize.',
              ),
              value: widget.showLivePreview,
              onChanged: widget.onShowLivePreviewChanged,
            ),
            const SizedBox(height: 12),
            _SectionLabel('Appearance'),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Dark picker theme'),
              value: config.isDark,
              onChanged: (value) => onChanged(config.copyWith(isDark: value)),
            ),
            _ColorRow(
              label: 'Accent color',
              value: config.seedColor,
              choices: const [
                Color(0xFF5B4FCF),
                Color(0xFF0F766E),
                Color(0xFFBE123C),
                Color(0xFF1D4ED8),
                Color(0xFFCA8A04),
              ],
              onChanged: (color) =>
                  onChanged(config.copyWith(seedColor: color)),
            ),
            _SliderRow(
              label: 'Corner radius',
              value: config.borderRadius,
              min: 4,
              max: 28,
              onChanged: (value) =>
                  onChanged(config.copyWith(borderRadius: value)),
            ),
            _SliderRow(
              label: 'Sidebar width',
              value: config.sidebarWidth,
              min: 160,
              max: 320,
              divisions: 8,
              onChanged: (value) =>
                  onChanged(config.copyWith(sidebarWidth: value)),
            ),
            const SizedBox(height: 8),
            _SectionLabel('Modal'),
            _EnumChips<FilePickerPresentation>(
              label: 'Presentation',
              value: config.presentation,
              values: FilePickerPresentation.values,
              labelFor: (value) => value.name,
              onChanged: (value) =>
                  onChanged(config.copyWith(presentation: value)),
            ),
            _EnumChips<FilePickerTransitionKind>(
              label: 'Transition',
              value: config.transitionKind,
              values: FilePickerTransitionKind.values,
              labelFor: (value) => value.name,
              onChanged: (value) =>
                  onChanged(config.copyWith(transitionKind: value)),
            ),
            _SliderRow(
              label: 'Modal max width',
              value: config.modalMaxWidth,
              min: 640,
              max: 1280,
              divisions: 8,
              onChanged: (value) =>
                  onChanged(config.copyWith(modalMaxWidth: value)),
            ),
            _SliderRow(
              label: 'Modal max height',
              value: config.modalMaxHeight,
              min: 480,
              max: 900,
              divisions: 7,
              onChanged: (value) =>
                  onChanged(config.copyWith(modalMaxHeight: value)),
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Dismiss on barrier tap'),
              value: config.barrierDismissible,
              onChanged: (value) =>
                  onChanged(config.copyWith(barrierDismissible: value)),
            ),
            const SizedBox(height: 8),
            _SectionLabel('Layout & content'),
            _EnumChips<FilePickerViewMode>(
              label: 'Initial view',
              value: config.initialViewMode,
              values: FilePickerViewMode.values,
              labelFor: (value) => value.name,
              onChanged: (value) =>
                  onChanged(config.copyWith(initialViewMode: value)),
            ),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                for (final mode in FilePickerViewMode.values)
                  FilterChip(
                    label: Text(mode.name),
                    selected: config.allowedViewModes.contains(mode),
                    onSelected: (selected) {
                      final modes = Set<FilePickerViewMode>.from(
                        config.allowedViewModes,
                      );
                      if (selected) {
                        modes.add(mode);
                      } else if (modes.length > 1) {
                        modes.remove(mode);
                      }
                      onChanged(config.copyWith(allowedViewModes: modes));
                    },
                  ),
              ],
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Show shortcuts sidebar'),
              value: config.showShortcutsSidebar,
              onChanged: (value) =>
                  onChanged(config.copyWith(showShortcutsSidebar: value)),
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Show thumbnails'),
              value: config.showThumbnails,
              onChanged: (value) =>
                  onChanged(config.copyWith(showThumbnails: value)),
            ),
            const SizedBox(height: 8),
            _SectionLabel('Labels & strings'),
            TextField(
              decoration: const InputDecoration(
                labelText: 'Dialog title',
                border: OutlineInputBorder(),
              ),
              controller: _titleController,
              onChanged: (value) =>
                  onChanged(config.copyWith(customTitle: value)),
            ),
            const SizedBox(height: 8),
            TextField(
              decoration: const InputDecoration(
                labelText: 'Confirm button',
                border: OutlineInputBorder(),
              ),
              controller: _confirmController,
              onChanged: (value) =>
                  onChanged(config.copyWith(customConfirmLabel: value)),
            ),
            const SizedBox(height: 8),
            TextField(
              decoration: const InputDecoration(
                labelText: 'Cancel button',
                border: OutlineInputBorder(),
              ),
              controller: _cancelController,
              onChanged: (value) =>
                  onChanged(config.copyWith(customCancelLabel: value)),
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Use brand string overrides'),
              subtitle: const Text('Extends EnglishFilePickerStrings'),
              value: config.useBrandStrings,
              onChanged: (value) =>
                  onChanged(config.copyWith(useBrandStrings: value)),
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Use custom shortcut icons'),
              value: config.useCustomIcons,
              onChanged: (value) =>
                  onChanged(config.copyWith(useCustomIcons: value)),
            ),
            const SizedBox(height: 8),
            DecoratedBox(
              decoration: BoxDecoration(
                color: scheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(12),
                border: Border.all(color: scheme.outlineVariant),
              ),
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Text(
                  'Active config: ${config.presentation.name} · '
                  '${config.transitionKind.name} · '
                  '${config.initialViewMode.name} · '
                  '${config.allowedViewModes.length} view modes',
                  style: Theme.of(context).textTheme.bodySmall,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

  final String text;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(
        text,
        style: Theme.of(context).textTheme.labelLarge?.copyWith(
              fontWeight: FontWeight.w700,
            ),
      ),
    );
  }
}

class _SliderRow extends StatelessWidget {
  const _SliderRow({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
    this.divisions,
  });

  final String label;
  final double value;
  final double min;
  final double max;
  final int? divisions;
  final ValueChanged<double> onChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('$label: ${value.round()}'),
        Slider(
          value: value,
          min: min,
          max: max,
          divisions: divisions,
          label: value.round().toString(),
          onChanged: onChanged,
        ),
      ],
    );
  }
}

class _ColorRow extends StatelessWidget {
  const _ColorRow({
    required this.label,
    required this.value,
    required this.choices,
    required this.onChanged,
  });

  final String label;
  final Color value;
  final List<Color> choices;
  final ValueChanged<Color> onChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            for (final color in choices)
              ChoiceChip(
                label: const SizedBox(width: 18, height: 18),
                selected: value.toARGB32() == color.toARGB32(),
                avatar: CircleAvatar(backgroundColor: color, radius: 10),
                onSelected: (_) => onChanged(color),
              ),
          ],
        ),
      ],
    );
  }
}

class _EnumChips<T> extends StatelessWidget {
  const _EnumChips({
    required this.label,
    required this.value,
    required this.values,
    required this.labelFor,
    required this.onChanged,
  });

  final String label;
  final T value;
  final List<T> values;
  final String Function(T value) labelFor;
  final ValueChanged<T> onChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: [
            for (final item in values)
              ChoiceChip(
                label: Text(labelFor(item)),
                selected: item == value,
                onSelected: (_) => onChanged(item),
              ),
          ],
        ),
        const SizedBox(height: 8),
      ],
    );
  }
}
1
likes
0
points
208
downloads

Publisher

verified publisherquds.cc

Weekly Downloads

Cross-platform Flutter file and folder picker with a unified, fully customizable explorer-style UI — list, details, tiles, and icons views, search, thumbnails, theming, and RTL on Android, iOS, and desktop.

Homepage
Repository (GitHub)
View/report issues

Topics

#file-picker #file-selector #folder-picker #flutter-plugin #explorer

License

unknown (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on unified_file_picker

Packages that implement unified_file_picker