torrserver_flutter 0.0.4 copy "torrserver_flutter: ^0.0.4" to clipboard
torrserver_flutter: ^0.0.4 copied to clipboard

A Flutter package wrapping TorrServer for streaming torrents via HTTP API on Windows, Linux, macOS, Android, and iOS.

example/lib/main.dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:torrserver_flutter/torrserver_flutter.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TorrServer Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const TorrServerHomePage(),
    );
  }
}

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

  @override
  State<TorrServerHomePage> createState() => _TorrServerHomePageState();
}

class _TorrServerHomePageState extends State<TorrServerHomePage> {
  final TorrServerController _controller = createTorrServerController();
  final TextEditingController _magnetController = TextEditingController(
    text:
        'magnet:?xt=urn:btih:4344503b7e797ebf31582327a5baae35b11bda01&dn=ubuntu-24.04-desktop-amd64.iso',
  );

  bool _isStarting = false;
  String _statusText = 'TorrServer stopped';
  String? _serverVersion;
  List<TorrentInfo> _torrents = [];
  Timer? _refreshTimer;

  @override
  void dispose() {
    _refreshTimer?.cancel();
    _magnetController.dispose();
    if (_controller.isRunning) {
      _controller.stop();
    }
    super.dispose();
  }

  Future<void> _startServer() async {
    setState(() {
      _isStarting = true;
      _statusText = 'Starting TorrServer...';
    });

    try {
      await _controller.start();
      final version = await _controller.echo();

      setState(() {
        _isStarting = false;
        _serverVersion = version;
        _statusText = 'Running at ${_controller.baseUrl} (TorrServer $version)';
      });

      _startAutoRefresh();
      await _refreshTorrents();
    } catch (e) {
      setState(() {
        _isStarting = false;
        _statusText = 'Error starting server: $e';
      });
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Start Error: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  Future<void> _stopServer() async {
    _refreshTimer?.cancel();
    setState(() {
      _statusText = 'Stopping TorrServer...';
    });

    try {
      await _controller.stop();
      setState(() {
        _serverVersion = null;
        _torrents = [];
        _statusText = 'TorrServer stopped';
      });
    } catch (e) {
      setState(() {
        _statusText = 'Error stopping server: $e';
      });
    }
  }

  void _startAutoRefresh() {
    _refreshTimer?.cancel();
    _refreshTimer = Timer.periodic(const Duration(seconds: 2), (_) {
      if (_controller.isRunning) {
        _refreshTorrents();
      }
    });
  }

  Future<void> _refreshTorrents() async {
    if (!_controller.isRunning) return;
    try {
      final list = await _controller.listTorrents();
      if (mounted) {
        setState(() {
          _torrents = list;
        });
      }
    } catch (_) {}
  }

  String _extractDisplayName(String magnet) {
    try {
      final match = RegExp(r'[?&]dn=([^&]+)').firstMatch(magnet);
      if (match != null && match.group(1) != null) {
        return Uri.decodeComponent(match.group(1)!.replaceAll('+', ' '));
      }
    } catch (_) {}
    return '';
  }

  Future<void> _addTorrent() async {
    final magnet = _magnetController.text.trim();
    if (magnet.isEmpty) return;

    final displayName = _extractDisplayName(magnet);

    try {
      final added = await _controller.addTorrent(
        magnet: magnet,
        title: displayName,
        saveToDb: true,
      );

      _magnetController.clear();

      if (mounted) {
        final title = added.title.isNotEmpty
            ? added.title
            : (displayName.isNotEmpty ? displayName : added.hash);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Added torrent: $title'),
            backgroundColor: Colors.green,
          ),
        );
      }
      await _refreshTorrents();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Failed to add torrent: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  Future<void> _removeTorrent(String hash) async {
    try {
      await _controller.removeTorrent(hash);
      await _refreshTorrents();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Failed to remove: $e'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  String _formatBytes(num bytes) {
    if (bytes <= 0) return '0 B';
    const suffixes = ['B', 'KB', 'MB', 'GB', 'TB'];
    final i = (log(bytes) / log(1024)).floor();
    final index = i.clamp(0, suffixes.length - 1);
    final size = bytes / pow(1024, index);
    return '${size.toStringAsFixed(index == 0 ? 0 : 2)} ${suffixes[index]}';
  }

  void _showStreamUrlDialog(TorrentInfo torrent, [TorrentFileStat? file]) {
    final fileIndex = file?.id ??
        (torrent.fileStats.isNotEmpty ? torrent.fileStats.first.id : 1);
    final streamUrl = _controller.streamUrl(torrent.hash, fileIndex: fileIndex);
    final fileLabel = file != null
        ? file.path
        : (torrent.title.isNotEmpty ? torrent.title : torrent.hash);

    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Row(
          children: [
            Icon(Icons.play_circle_fill, color: Colors.green, size: 28),
            SizedBox(width: 8),
            Expanded(
              child: Text(
                'Playable HTTP Stream URL',
                style: TextStyle(fontSize: 18),
              ),
            ),
          ],
        ),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'File #$fileIndex: $fileLabel',
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 14,
                color: Colors.white,
              ),
            ),
            if (file != null && file.length > 0) ...[
              const SizedBox(height: 4),
              Text(
                'Size: ${_formatBytes(file.length)}',
                style: const TextStyle(fontSize: 12, color: Colors.white70),
              ),
            ],
            const SizedBox(height: 12),
            const Text(
              'Use this direct stream URL in VLC, MPV, media_kit, or video_player:',
              style: TextStyle(fontSize: 12, color: Colors.white60),
            ),
            const SizedBox(height: 8),
            Container(
              padding: const EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: Colors.black45,
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: Colors.white24),
              ),
              child: SelectableText(
                streamUrl.toString(),
                style: const TextStyle(
                  fontFamily: 'monospace',
                  fontSize: 12,
                  color: Colors.lightGreenAccent,
                ),
              ),
            ),
          ],
        ),
        actions: [
          TextButton.icon(
            icon: const Icon(Icons.copy, size: 18),
            label: const Text('Copy URL'),
            onPressed: () {
              Clipboard.setData(ClipboardData(text: streamUrl.toString()));
              Navigator.of(ctx).pop();
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text('Stream URL copied to clipboard!'),
                  backgroundColor: Colors.deepPurple,
                ),
              );
            },
          ),
          TextButton(
            child: const Text('Close'),
            onPressed: () => Navigator.of(ctx).pop(),
          ),
        ],
      ),
    );
  }

  void _showLicensesDialog() {
    showLicensePage(
      context: context,
      applicationName: 'TorrServer Flutter Demo',
      applicationVersion: '0.0.1',
      applicationLegalese:
          'TorrServer is licensed under GNU General Public License v3.0 (GPL-3.0).\n'
          'The iOS build statically embeds TorrServer and inherits GPLv3 source obligations.',
    );
  }

  IconData _getFileIcon(String path) {
    final lower = path.toLowerCase();
    if (lower.endsWith('.mp4') ||
        lower.endsWith('.mkv') ||
        lower.endsWith('.avi') ||
        lower.endsWith('.mov') ||
        lower.endsWith('.webm') ||
        lower.endsWith('.ts')) {
      return Icons.video_file;
    } else if (lower.endsWith('.mp3') ||
        lower.endsWith('.flac') ||
        lower.endsWith('.wav') ||
        lower.endsWith('.aac') ||
        lower.endsWith('.m4a')) {
      return Icons.audio_file;
    } else if (lower.endsWith('.srt') ||
        lower.endsWith('.vtt') ||
        lower.endsWith('.ass') ||
        lower.endsWith('.sub')) {
      return Icons.subtitles;
    } else if (lower.endsWith('.iso') ||
        lower.endsWith('.img') ||
        lower.endsWith('.bin')) {
      return Icons.album;
    } else if (lower.endsWith('.zip') ||
        lower.endsWith('.rar') ||
        lower.endsWith('.7z') ||
        lower.endsWith('.tar') ||
        lower.endsWith('.gz')) {
      return Icons.folder_zip;
    }
    return Icons.insert_drive_file;
  }

  @override
  Widget build(BuildContext context) {
    final isRunning = _controller.isRunning;

    return Scaffold(
      appBar: AppBar(
        title: const Text('TorrServer Flutter'),
        actions: [
          IconButton(
            icon: const Icon(Icons.info_outline),
            tooltip: 'Open Source Licenses (GPL-3.0)',
            onPressed: _showLicensesDialog,
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Status Card
            Card(
              elevation: 2,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(
                          isRunning ? Icons.check_circle : Icons.stop_circle,
                          color: isRunning ? Colors.green : Colors.redAccent,
                        ),
                        const SizedBox(width: 8),
                        Expanded(
                          child: Text(
                            _statusText,
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    Row(
                      children: [
                        ElevatedButton.icon(
                          icon: Icon(isRunning ? Icons.stop : Icons.play_arrow),
                          label:
                              Text(isRunning ? 'Stop Server' : 'Start Server'),
                          style: ElevatedButton.styleFrom(
                            backgroundColor: isRunning
                                ? Colors.redAccent
                                : Colors.deepPurple,
                            foregroundColor: Colors.white,
                          ),
                          onPressed: _isStarting
                              ? null
                              : isRunning
                                  ? _stopServer
                                  : _startServer,
                        ),
                        const SizedBox(width: 12),
                        if (isRunning)
                          OutlinedButton.icon(
                            icon: const Icon(Icons.refresh),
                            label: const Text('Refresh'),
                            onPressed: _refreshTorrents,
                          ),
                        if (_serverVersion != null) ...[
                          const SizedBox(width: 12),
                          Chip(
                            label: Text('v$_serverVersion'),
                            visualDensity: VisualDensity.compact,
                          ),
                        ],
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Magnet input
            if (isRunning) ...[
              Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: _magnetController,
                      decoration: const InputDecoration(
                        labelText: 'Magnet Link or Torrent Hash',
                        hintText: 'Paste magnet:?xt=urn:btih:...',
                        border: OutlineInputBorder(),
                        isDense: true,
                      ),
                      onSubmitted: (_) => _addTorrent(),
                    ),
                  ),
                  const SizedBox(width: 8),
                  FilledButton.icon(
                    icon: const Icon(Icons.add),
                    label: const Text('Add'),
                    onPressed: _addTorrent,
                  ),
                ],
              ),
              const SizedBox(height: 16),
            ],

            // Torrent List Header
            Text(
              'Active Torrents (${_torrents.length})',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),

            // Torrent List View
            Expanded(
              child: _torrents.isEmpty
                  ? Center(
                      child: Text(
                        isRunning
                            ? 'No active torrents. Add a magnet link above.'
                            : 'Start TorrServer to view and manage torrents.',
                        style: const TextStyle(color: Colors.white54),
                      ),
                    )
                  : ListView.builder(
                      itemCount: _torrents.length,
                      itemBuilder: (ctx, index) {
                        final t = _torrents[index];
                        final speedStr = _formatBytes(t.downloadSpeed);
                        final totalStr = _formatBytes(t.torrentSize);
                        final loadedStr = _formatBytes(t.loadedSize);
                        final progress = t.torrentSize > 0
                            ? (t.loadedSize / t.torrentSize).clamp(0.0, 1.0)
                            : 0.0;

                        final title = t.title.isNotEmpty ? t.title : t.hash;

                        return Card(
                          margin: const EdgeInsets.symmetric(vertical: 6),
                          clipBehavior: Clip.antiAlias,
                          child: ExpansionTile(
                            leading: Icon(
                              progress >= 1.0
                                  ? Icons.check_circle_outline
                                  : (t.downloadSpeed > 0
                                      ? Icons.downloading
                                      : Icons.folder),
                              color: progress >= 1.0
                                  ? Colors.green
                                  : (t.downloadSpeed > 0
                                      ? Colors.lightBlueAccent
                                      : Colors.white70),
                            ),
                            title: Text(
                              title,
                              style:
                                  const TextStyle(fontWeight: FontWeight.w600),
                            ),
                            subtitle: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                const SizedBox(height: 4),
                                Row(
                                  children: [
                                    Container(
                                      padding: const EdgeInsets.symmetric(
                                          horizontal: 6, vertical: 2),
                                      decoration: BoxDecoration(
                                        color: Colors.white12,
                                        borderRadius: BorderRadius.circular(4),
                                      ),
                                      child: Text(
                                        t.statString,
                                        style: const TextStyle(
                                            fontSize: 11,
                                            fontWeight: FontWeight.bold),
                                      ),
                                    ),
                                    const SizedBox(width: 8),
                                    Text(
                                      'Peers: ${t.activePeers}/${t.totalPeers}',
                                      style: const TextStyle(fontSize: 12),
                                    ),
                                    const Spacer(),
                                    Text(
                                      '$speedStr/s',
                                      style: const TextStyle(
                                        fontSize: 12,
                                        color: Colors.lightGreenAccent,
                                        fontWeight: FontWeight.bold,
                                      ),
                                    ),
                                  ],
                                ),
                                const SizedBox(height: 6),
                                LinearProgressIndicator(
                                  value: progress,
                                  backgroundColor: Colors.white10,
                                  valueColor: AlwaysStoppedAnimation(
                                    progress >= 1.0
                                        ? Colors.green
                                        : Colors.deepPurpleAccent,
                                  ),
                                ),
                                const SizedBox(height: 4),
                                Text(
                                  '$loadedStr / $totalStr (${(progress * 100).toStringAsFixed(1)}%) • ${t.fileStats.length} files',
                                  style: const TextStyle(
                                      fontSize: 11, color: Colors.white60),
                                ),
                              ],
                            ),
                            trailing: Row(
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                IconButton(
                                  icon: const Icon(Icons.play_arrow,
                                      color: Colors.green),
                                  tooltip: 'Stream First File',
                                  onPressed: () => _showStreamUrlDialog(t),
                                ),
                                IconButton(
                                  icon: const Icon(Icons.delete_outline,
                                      color: Colors.redAccent),
                                  tooltip: 'Remove Torrent',
                                  onPressed: () => _removeTorrent(t.hash),
                                ),
                              ],
                            ),
                            children: [
                              const Divider(height: 1),
                              Container(
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 16, vertical: 8),
                                alignment: Alignment.centerLeft,
                                child: Text(
                                  'Files in Torrent (${t.fileStats.length})',
                                  style: const TextStyle(
                                    fontWeight: FontWeight.bold,
                                    fontSize: 13,
                                    color: Colors.white70,
                                  ),
                                ),
                              ),
                              if (t.fileStats.isEmpty)
                                const Padding(
                                  padding: EdgeInsets.all(16.0),
                                  child: Text(
                                    'Fetching file metadata from peers...',
                                    style: TextStyle(
                                        fontSize: 12,
                                        fontStyle: FontStyle.italic,
                                        color: Colors.white54),
                                  ),
                                )
                              else
                                ListView.separated(
                                  shrinkWrap: true,
                                  physics: const NeverScrollableScrollPhysics(),
                                  itemCount: t.fileStats.length,
                                  separatorBuilder: (_, __) =>
                                      const Divider(height: 1, indent: 48),
                                  itemBuilder: (ctx, fileIdx) {
                                    final file = t.fileStats[fileIdx];
                                    final fileIcon = _getFileIcon(file.path);
                                    return ListTile(
                                      dense: true,
                                      leading: Icon(fileIcon,
                                          size: 20,
                                          color: Colors.deepPurpleAccent),
                                      title: Text(
                                        file.path,
                                        style: const TextStyle(fontSize: 13),
                                      ),
                                      subtitle: Text(
                                        'File #${file.id} • ${_formatBytes(file.length)}',
                                        style: const TextStyle(
                                            fontSize: 11,
                                            color: Colors.white60),
                                      ),
                                      trailing: FilledButton.tonalIcon(
                                        icon: const Icon(Icons.play_arrow,
                                            size: 16),
                                        label: const Text('Stream'),
                                        style: FilledButton.styleFrom(
                                          visualDensity: VisualDensity.compact,
                                          padding: const EdgeInsets.symmetric(
                                              horizontal: 8),
                                        ),
                                        onPressed: () =>
                                            _showStreamUrlDialog(t, file),
                                      ),
                                    );
                                  },
                                ),
                            ],
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
0
points
1.03k
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package wrapping TorrServer for streaming torrents via HTTP API on Windows, Linux, macOS, Android, and iOS.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

ffi, flutter, http, meta, path, path_provider

More

Packages that depend on torrserver_flutter

Packages that implement torrserver_flutter