opensubtitles_hasher 1.1.0 copy "opensubtitles_hasher: ^1.1.0" to clipboard
opensubtitles_hasher: ^1.1.0 copied to clipboard

PlatformAndroid

Fast OpenSubtitles hashing (128 KB only). Includes a built-in movie picker for all platforms - Android folder browser, iOS, macOS, Windows, and Linux.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:opensubtitles_hasher/opensubtitles_hasher.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'opensubtitles_hasher',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0B6E4F)),
        useMaterial3: true,
      ),
      home: const DemoPage(),
    );
  }
}

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

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  final _pathController = TextEditingController();

  /// Folder browser on by default (mediaStore). Off → system Documents UI.
  bool _folderBrowser = true;

  /// 0 = filter off.
  int _minDurationMinutes = 0;

  /// 0 = filter off.
  int _minSizeMb = 0;

  /// Selected MIME keys. Empty / only all → video/*.
  final Set<String> _mimeKeys = {'all'};

  String? _hash;
  int? _fileSize;
  String? _info;
  String? _error;
  bool _loading = false;

  bool get _isAndroid => !kIsWeb && Platform.isAndroid;

  static const _mimeChoices = <String, String>{
    'all': 'video/*',
    'mp4': 'video/mp4',
    'mkv': 'video/x-matroska',
    'webm': 'video/webm',
    'avi': 'video/avi',
  };

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

  MoviePickerOptions get _options {
    final mimes = _mimeKeys.contains('all') || _mimeKeys.isEmpty
        ? MoviePickerOptions.defaultMimeTypes
        : _mimeKeys.map((k) => _mimeChoices[k]!).toList();

    return MoviePickerOptions(
      mode: _folderBrowser
          ? MoviePickerMode.mediaStore
          : MoviePickerMode.systemDocuments,
      mimeTypes: mimes,
      minDuration: _minDurationMinutes > 0
          ? Duration(minutes: _minDurationMinutes)
          : null,
      minSizeBytes: _minSizeMb > 0 ? _minSizeMb * 1024 * 1024 : null,
    );
  }

  Future<void> _computeHash() async {
    final path = _pathController.text.trim();
    if (path.isEmpty) {
      setState(() {
        _error = 'Enter a file path or content:// URI';
        _hash = null;
        _fileSize = null;
        _info = null;
      });
      return;
    }
    await _runHash(path);
  }

  Future<void> _pickAndHash() async {
    setState(() {
      _loading = true;
      _error = null;
      _hash = null;
      _fileSize = null;
      _info = null;
    });

    try {
      final picked = await OpenSubtitlesHasher.pickAndHash(options: _options);
      if (picked == null) {
        setState(() => _info = 'Cancelled');
        return;
      }

      _pathController.text = picked.movie.effectivePath;
      final duration = picked.movie.duration;
      setState(() {
        _hash = picked.hash.hash;
        _fileSize = picked.hash.fileSize;
        _info = [
          picked.movie.name ?? 'Selected video',
          if (duration != null) _formatDuration(duration),
        ].join(' · ');
      });
    } on MovieFilterException catch (e) {
      setState(() => _error = '${e.code}: ${e.message}');
    } on UnsupportedError catch (e) {
      setState(() => _error = e.message);
    } on PlatformException catch (e) {
      setState(() => _error = '${e.code}: ${e.message}');
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      setState(() => _loading = false);
    }
  }

  Future<void> _runHash(String path) async {
    setState(() {
      _loading = true;
      _error = null;
      _hash = null;
      _fileSize = null;
      _info = null;
    });

    try {
      final result = await OpenSubtitlesHasher.computeHashResult(path);
      setState(() {
        _hash = result.hash;
        _fileSize = result.fileSize;
      });
    } on FileSystemException catch (e) {
      setState(() => _error = 'File error: ${e.message}');
    } on InvalidFileException catch (e) {
      setState(() => _error = e.message);
    } on PlatformException catch (e) {
      setState(() => _error = '${e.code}: ${e.message}');
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      setState(() => _loading = false);
    }
  }

  void _nudgeDuration(int delta) {
    setState(() {
      _minDurationMinutes = (_minDurationMinutes + delta).clamp(0, 300);
    });
  }

  void _nudgeSize(int delta) {
    setState(() {
      _minSizeMb = (_minSizeMb + delta).clamp(0, 5000);
    });
  }

  void _toggleMime(String key) {
    setState(() {
      if (key == 'all') {
        _mimeKeys
          ..clear()
          ..add('all');
        return;
      }
      _mimeKeys.remove('all');
      if (!_mimeKeys.add(key)) {
        _mimeKeys.remove(key);
      }
      if (_mimeKeys.isEmpty) {
        _mimeKeys.add('all');
      }
    });
  }

  String _formatDuration(Duration d) {
    final h = d.inHours;
    final m = d.inMinutes.remainder(60);
    final s = d.inSeconds.remainder(60);
    if (h > 0) return '${h}h ${m.toString().padLeft(2, '0')}m';
    return '${m}m ${s.toString().padLeft(2, '0')}s';
  }

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

    return Scaffold(
      appBar: AppBar(title: const Text('opensubtitles_hasher')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text('Hash from path', style: theme.textTheme.titleMedium),
          const SizedBox(height: 8),
          TextField(
            controller: _pathController,
            decoration: const InputDecoration(
              border: OutlineInputBorder(),
              labelText: 'Path or content:// URI',
              hintText: '/path/to/movie.mp4',
            ),
            autocorrect: false,
            enableSuggestions: false,
          ),
          const SizedBox(height: 8),
          FilledButton(
            onPressed: _loading ? null : _computeHash,
            child: _loading
                ? const SizedBox(
                    height: 20,
                    width: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Compute hash'),
          ),
          const SizedBox(height: 28),
          Text(
            'Movie picker',
            style: theme.textTheme.titleMedium,
          ),
          const SizedBox(height: 4),
          Text(
            _isAndroid
                ? 'Folder browser lists only folders that contain videos '
                    'matching your filters. Raise min duration to hide short clips.'
                : 'System file dialog filtered by video extension. '
                    'Duration / size filters are Android-only.',
            style: theme.textTheme.bodySmall,
          ),
          if (_isAndroid) ...[
            const SizedBox(height: 12),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Folder browser'),
              subtitle: Text(
                _folderBrowser
                    ? 'On - folders with matching videos only'
                    : 'Off - system Documents UI',
              ),
              value: _folderBrowser,
              onChanged: (v) => setState(() => _folderBrowser = v),
            ),
            const SizedBox(height: 8),
            _StepperRow(
              label: 'Min duration',
              valueLabel: _minDurationMinutes == 0
                  ? 'Off'
                  : '$_minDurationMinutes min',
              onDecrement: () => _nudgeDuration(-15),
              onIncrement: () => _nudgeDuration(15),
            ),
            const SizedBox(height: 8),
            _StepperRow(
              label: 'Min size',
              valueLabel: _minSizeMb == 0 ? 'Off' : '$_minSizeMb MB',
              onDecrement: () => _nudgeSize(-50),
              onIncrement: () => _nudgeSize(50),
            ),
            const SizedBox(height: 12),
            Text('Types', style: theme.textTheme.titleSmall),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: _mimeChoices.keys.map((key) {
                final selected = _mimeKeys.contains(key);
                return FilterChip(
                  label: Text(key == 'all' ? 'All videos' : key.toUpperCase()),
                  selected: selected,
                  onSelected: (_) => _toggleMime(key),
                );
              }).toList(),
            ),
          ],
          const SizedBox(height: 16),
          OutlinedButton.icon(
            onPressed: _loading ? null : _pickAndHash,
            icon: const Icon(Icons.movie_outlined),
            label: const Text('Pick movie + hash'),
          ),
          const SizedBox(height: 24),
          if (_info != null) Text(_info!),
          if (_hash != null) ...[
            Text('Hash', style: theme.textTheme.titleMedium),
            SelectableText(_hash!, style: theme.textTheme.titleLarge),
            const SizedBox(height: 8),
            Text('$_fileSize bytes'),
            const SizedBox(height: 4),
            SelectableText(
              '{moviehash: $_hash, moviebytesize: $_fileSize}',
              style: theme.textTheme.bodySmall,
            ),
          ],
          if (_error != null) ...[
            const SizedBox(height: 8),
            Text(
              _error!,
              style: TextStyle(color: theme.colorScheme.error),
            ),
          ],
        ],
      ),
    );
  }
}

class _StepperRow extends StatelessWidget {
  const _StepperRow({
    required this.label,
    required this.valueLabel,
    required this.onDecrement,
    required this.onIncrement,
  });

  final String label;
  final String valueLabel;
  final VoidCallback onDecrement;
  final VoidCallback onIncrement;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Text(label, style: Theme.of(context).textTheme.titleSmall),
        ),
        IconButton.filledTonal(
          onPressed: onDecrement,
          icon: const Icon(Icons.remove),
          tooltip: 'Decrease',
        ),
        SizedBox(
          width: 88,
          child: Text(
            valueLabel,
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.titleSmall,
          ),
        ),
        IconButton.filledTonal(
          onPressed: onIncrement,
          icon: const Icon(Icons.add),
          tooltip: 'Increase',
        ),
      ],
    );
  }
}
1
likes
160
points
141
downloads

Documentation

API reference

Publisher

verified publisherdarkmintis.dev

Weekly Downloads

Fast OpenSubtitles hashing (128 KB only). Includes a built-in movie picker for all platforms - Android folder browser, iOS, macOS, Windows, and Linux.

Repository (GitHub)
View/report issues

Topics

#opensubtitles #hash #subtitle #video #media

License

MIT (license)

Dependencies

file_picker, flutter

More

Packages that depend on opensubtitles_hasher

Packages that implement opensubtitles_hasher