Skip to main content

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

Vertical short-video feed and series episode player for Flutter with park/promote preloading. No backend required.

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();
  MockFormatFilter _selected = MockFormatFilter.mp4;

  @override
  Widget build(BuildContext context) {
    final l10n = DemoLocaleScope.l10nOf(context);
    final selectedCount = MockVideos.clipsFor(_selected).length;
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: ListView(
                padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
                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.selectVideoType,
                    style: const TextStyle(
                      fontSize: 15,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                  const SizedBox(height: 6),
                  Text(
                    l10n.introHint,
                    style: TextStyle(
                      color: Colors.white.withValues(alpha: 0.45),
                      height: 1.4,
                      fontSize: 13,
                    ),
                  ),
                  const SizedBox(height: 14),
                  _VideoTypeList(
                    selected: _selected,
                    onSelected: (f) => setState(() => _selected = f),
                  ),
                  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),
                    ),
                  ),
                ],
              ),
            ),
            _PlayActionsBar(
              selectedHint: l10n.selectedTypeHint(
                l10n.videoTypeTitle(_selected),
                selectedCount,
              ),
              playFeedLabel: l10n.playFeed,
              playEpisodesLabel: l10n.playEpisodes,
              onPlayFeed: () => _openFeed(_selected),
              onPlayEpisodes: () => _openEpisodes(_selected),
            ),
          ],
        ),
      ),
    );
  }

  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 _PlayActionsBar extends StatelessWidget {
  const _PlayActionsBar({
    required this.selectedHint,
    required this.playFeedLabel,
    required this.playEpisodesLabel,
    required this.onPlayFeed,
    required this.onPlayEpisodes,
  });

  final String selectedHint;
  final String playFeedLabel;
  final String playEpisodesLabel;
  final VoidCallback onPlayFeed;
  final VoidCallback onPlayEpisodes;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: const Color(0xFF0B0B0F),
        border: Border(
          top: BorderSide(color: Colors.white.withValues(alpha: 0.08)),
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.35),
            blurRadius: 16,
            offset: const Offset(0, -4),
          ),
        ],
      ),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(24, 12, 24, 8),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              selectedHint,
              style: const TextStyle(
                fontSize: 13,
                color: Colors.white,
              ),
            ),
            const SizedBox(height: 10),
            Row(
              children: [
                Expanded(
                  child: FilledButton.tonal(
                    onPressed: onPlayFeed,
                    child: Text(playFeedLabel),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: FilledButton.tonal(
                    onPressed: onPlayEpisodes,
                    child: Text(playEpisodesLabel),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _VideoTypeList extends StatelessWidget {
  const _VideoTypeList({
    required this.selected,
    required this.onSelected,
  });

  final MockFormatFilter selected;
  final ValueChanged<MockFormatFilter> onSelected;

  static Color _color(MockFormatFilter f) => switch (f) {
        MockFormatFilter.mp4 => const Color(0xFF38BDF8),
        MockFormatFilter.hls => const Color(0xFF34D399),
        MockFormatFilter.webm => const Color(0xFFFBBF24),
        MockFormatFilter.mov => const Color(0xFFF472B6),
        MockFormatFilter.m4v => const Color(0xFFA78BFA),
        MockFormatFilter.dash => const Color(0xFFFB923C),
        MockFormatFilter.all => const Color(0xFFE2E8F0),
      };

  static IconData _icon(MockFormatFilter f) => switch (f) {
        MockFormatFilter.mp4 => Icons.movie_outlined,
        MockFormatFilter.hls => Icons.playlist_play,
        MockFormatFilter.webm => Icons.web_asset,
        MockFormatFilter.mov => Icons.videocam_outlined,
        MockFormatFilter.m4v => Icons.video_file_outlined,
        MockFormatFilter.dash => Icons.stream,
        MockFormatFilter.all => Icons.library_books_outlined,
      };

  @override
  Widget build(BuildContext context) {
    final l10n = DemoLocaleScope.l10nOf(context);
    final order = MockVideos.availableFilters;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          MockVideos.platformHint(),
          style: TextStyle(
            fontSize: 12,
            height: 1.35,
            color: Colors.white.withValues(alpha: 0.4),
          ),
        ),
        const SizedBox(height: 10),
        Container(
          decoration: BoxDecoration(
            color: const Color(0xFF17171C),
            borderRadius: BorderRadius.circular(14),
            border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
          ),
          child: Column(
            children: [
              for (var i = 0; i < order.length; i++) ...[
                if (i > 0)
                  Divider(
                    height: 1,
                    color: Colors.white.withValues(alpha: 0.06),
                  ),
                _VideoTypeTile(
                  title: l10n.videoTypeTitle(order[i]),
                  subtitle: l10n.videoTypeSubtitle(
                    order[i],
                    MockVideos.clipsFor(order[i]).length,
                  ),
                  color: _color(order[i]),
                  icon: _icon(order[i]),
                  selected: selected == order[i],
                  onTap: () => onSelected(order[i]),
                ),
              ],
            ],
          ),
        ),
      ],
    );
  }
}

class _VideoTypeTile extends StatelessWidget {
  const _VideoTypeTile({
    required this.title,
    required this.subtitle,
    required this.color,
    required this.icon,
    required this.selected,
    required this.onTap,
  });

  final String title;
  final String subtitle;
  final Color color;
  final IconData icon;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: selected ? color.withValues(alpha: 0.12) : Colors.transparent,
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
          child: Row(
            children: [
              Container(
                width: 36,
                height: 36,
                decoration: BoxDecoration(
                  color: color.withValues(alpha: selected ? 0.28 : 0.14),
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Icon(icon, size: 18, color: color),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: TextStyle(
                        fontWeight: FontWeight.w700,
                        color: selected ? color : Colors.white,
                      ),
                    ),
                    const SizedBox(height: 2),
                    Text(
                      subtitle,
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.white.withValues(alpha: 0.45),
                      ),
                    ),
                  ],
                ),
              ),
              Icon(
                selected
                    ? Icons.radio_button_checked
                    : Icons.radio_button_off,
                size: 20,
                color: selected
                    ? color
                    : Colors.white.withValues(alpha: 0.35),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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,
        ),
      ],
    );
  }
}
1
likes
0
points
700
downloads

Publisher

unverified uploader

Weekly Downloads

Vertical short-video feed and series episode player for Flutter with park/promote preloading. No backend required.

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