Skip to main content

flutter_feed_player 0.1.1 copy "flutter_feed_player: ^0.1.1" to clipboard
flutter_feed_player: ^0.1.1 copied to clipboard

Vertical short-video feed player and series episode player for Flutter. Built on video_player with park/promote preloading for TikTok-style feeds and binge episode swiping. No backend required — host [...]

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feed_player/flutter_feed_player.dart';

import 'l10n/demo_l10n.dart';
import 'mock/mock_data.dart';
import 'pages/episode_demo_page.dart';
import 'pages/feed_demo_page.dart';
import 'widgets/player_param_panel.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
  ]);
  await PlaybackMemory.init();
  runApp(const FeedPlayerDemoApp());
}

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

  @override
  State<FeedPlayerDemoApp> createState() => _FeedPlayerDemoAppState();
}

class _FeedPlayerDemoAppState extends State<FeedPlayerDemoApp> {
  final DemoLocaleController _locale = DemoLocaleController();

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

  @override
  Widget build(BuildContext context) {
    return DemoLocaleScope(
      controller: _locale,
      child: ListenableBuilder(
        listenable: _locale,
        builder: (context, _) {
          return MaterialApp(
            title: _locale.l10n.appTitle,
            locale: _locale.lang.locale,
            debugShowCheckedModeBanner: false,
            theme: ThemeData(
              colorScheme: ColorScheme.fromSeed(
                seedColor: const Color(0xFF0F766E),
                brightness: Brightness.dark,
              ),
              useMaterial3: true,
              scaffoldBackgroundColor: const Color(0xFF0B0B0F),
            ),
            home: const DemoHomePage(),
          );
        },
      ),
    );
  }
}

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

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage> {
  PlayerDemoParams _params = const PlayerDemoParams();

  @override
  Widget build(BuildContext context) {
    final l10n = DemoLocaleScope.l10nOf(context);
    return Scaffold(
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
          children: [
            const _LanguageSwitcher(),
            const SizedBox(height: 16),
            Text(
              l10n.brandName,
              style: const TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.w800,
                letterSpacing: -0.5,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              l10n.tagline,
              style: TextStyle(
                color: Colors.white.withValues(alpha: 0.65),
                height: 1.4,
              ),
            ),
            const SizedBox(height: 28),
            Text(
              l10n.introHint,
              style: TextStyle(
                color: Colors.white.withValues(alpha: 0.45),
                height: 1.4,
                fontSize: 13,
              ),
            ),
            const SizedBox(height: 16),
            _FormatGroupCard(
              title: 'MP4',
              subtitle: l10n.mp4Subtitle(MockVideos.mp4Samples.length),
              color: const Color(0xFF38BDF8),
              playFeedLabel: l10n.playFeed,
              playEpisodesLabel: l10n.playEpisodes,
              onPlayFeed: () => _openFeed(MockFormatFilter.mp4),
              onPlayEpisodes: () => _openEpisodes(MockFormatFilter.mp4),
            ),
            const SizedBox(height: 16),
            _FormatGroupCard(
              title: 'M3U8 / HLS',
              subtitle: l10n.hlsSubtitle(MockVideos.hlsSamples.length),
              color: const Color(0xFF34D399),
              playFeedLabel: l10n.playFeed,
              playEpisodesLabel: l10n.playEpisodes,
              onPlayFeed: () => _openFeed(MockFormatFilter.hls),
              onPlayEpisodes: () => _openEpisodes(MockFormatFilter.hls),
            ),
            const SizedBox(height: 24),
            OutlinedButton.icon(
              onPressed: () => _openFeed(MockFormatFilter.all),
              icon: const Icon(Icons.swipe_vertical),
              label: Text(l10n.mixedFeed),
            ),
            const SizedBox(height: 10),
            OutlinedButton.icon(
              onPressed: () => _openEpisodes(MockFormatFilter.all),
              icon: const Icon(Icons.play_circle_outline),
              label: Text(l10n.mixedEpisodes),
            ),
            const SizedBox(height: 24),
            PlayerParamPanel(
              params: _params,
              onChanged: (p) => setState(() => _params = p),
            ),
            const SizedBox(height: 28),
            Text(
              l10n.sourcesFooter,
              style: TextStyle(
                fontSize: 12,
                color: Colors.white.withValues(alpha: 0.4),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _openFeed(MockFormatFilter filter) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => FeedDemoPage(
          formatFilter: filter,
          params: _params,
        ),
      ),
    );
  }

  void _openEpisodes(MockFormatFilter filter) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => EpisodeDemoPage(
          formatFilter: filter,
          params: _params,
        ),
      ),
    );
  }
}

class _LanguageSwitcher extends StatelessWidget {
  const _LanguageSwitcher();

  @override
  Widget build(BuildContext context) {
    final localeCtrl = DemoLocaleScope.of(context);
    final l10n = localeCtrl.l10n;
    return Row(
      children: [
        Icon(
          Icons.language,
          size: 18,
          color: Colors.white.withValues(alpha: 0.55),
        ),
        const SizedBox(width: 8),
        Text(
          l10n.language,
          style: TextStyle(
            fontSize: 13,
            fontWeight: FontWeight.w600,
            color: Colors.white.withValues(alpha: 0.55),
          ),
        ),
        const Spacer(),
        SegmentedButton<DemoLang>(
          segments: [
            for (final lang in DemoLang.values)
              ButtonSegment<DemoLang>(
                value: lang,
                label: Text(
                  lang.label,
                  style: const TextStyle(fontSize: 12),
                ),
              ),
          ],
          selected: {localeCtrl.lang},
          onSelectionChanged: (set) {
            if (set.isEmpty) return;
            localeCtrl.setLang(set.first);
          },
          style: ButtonStyle(
            visualDensity: VisualDensity.compact,
            tapTargetSize: MaterialTapTargetSize.shrinkWrap,
            foregroundColor: WidgetStateProperty.resolveWith((states) {
              if (states.contains(WidgetState.selected)) {
                return Colors.black;
              }
              return Colors.white70;
            }),
            backgroundColor: WidgetStateProperty.resolveWith((states) {
              if (states.contains(WidgetState.selected)) {
                return Colors.white;
              }
              return Colors.white.withValues(alpha: 0.08);
            }),
          ),
          showSelectedIcon: false,
        ),
      ],
    );
  }
}

class _FormatGroupCard extends StatelessWidget {
  const _FormatGroupCard({
    required this.title,
    required this.subtitle,
    required this.color,
    required this.playFeedLabel,
    required this.playEpisodesLabel,
    required this.onPlayFeed,
    required this.onPlayEpisodes,
  });

  final String title;
  final String subtitle;
  final Color color;
  final String playFeedLabel;
  final String playEpisodesLabel;
  final VoidCallback onPlayFeed;
  final VoidCallback onPlayEpisodes;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: const Color(0xFF17171C),
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: color.withValues(alpha: 0.35)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
                decoration: BoxDecoration(
                  color: color.withValues(alpha: 0.2),
                  borderRadius: BorderRadius.circular(6),
                ),
                child: Text(
                  title,
                  style: TextStyle(
                    color: color,
                    fontWeight: FontWeight.w800,
                    letterSpacing: 0.4,
                  ),
                ),
              ),
              const SizedBox(width: 10),
              Expanded(
                child: Text(
                  subtitle,
                  style: TextStyle(
                    fontSize: 12,
                    color: Colors.white.withValues(alpha: 0.45),
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: FilledButton.tonal(
                  onPressed: onPlayFeed,
                  child: Text(playFeedLabel),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: FilledButton.tonal(
                  onPressed: onPlayEpisodes,
                  child: Text(playEpisodesLabel),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
1
likes
0
points
700
downloads

Publisher

unverified uploader

Weekly Downloads

Vertical short-video feed player and series episode player for Flutter. Built on video_player with park/promote preloading for TikTok-style feeds and binge episode swiping. No backend required — host apps inject playable data.

Repository (GitHub)
View/report issues

Topics

#video #player #feed #short-video #flutter

License

unknown (license)

Dependencies

flutter, video_player

More

Packages that depend on flutter_feed_player