import 'package:flutter/material.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/player.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/menu.dart'; import 'package:freezer/translations.i18n.dart'; import 'tiles.dart'; import 'details_screens.dart'; import '../settings.dart'; class _SearchHeaderDelegate extends SliverPersistentHeaderDelegate { @override double get maxExtent => 76.0; @override double get minExtent => 76.0; @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => false; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return Padding( padding: const EdgeInsets.all(8.0), child: Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(56.0)), child: ListTile( mouseCursor: MaterialStateMouseCursor.textable, leading: const Icon(Icons.search), title: Text('Search or paste URL'.i18n), onTap: () => Navigator.of(context).pushNamed('/search')))); } } class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( body: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, _) => [ SliverPersistentHeader( delegate: _SearchHeaderDelegate(), floating: true) ], body: HomePageScreen(cacheable: true), ), ), ); } } class FreezerTitle extends StatelessWidget { const FreezerTitle({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(0, 24, 0, 8), child: LayoutBuilder(builder: (context, constraints) { return Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset('assets/icon.png', width: 64, height: 64), Text( 'freezer', style: TextStyle(fontSize: 56, fontWeight: FontWeight.w900), ) ], ); }), ); } } class HomePageScreen extends StatefulWidget { final HomePage? homePage; final bool cacheable; final DeezerChannel? channel; HomePageScreen( {this.homePage, this.channel, this.cacheable = false, Key? key}) : super(key: key); @override _HomePageScreenState createState() => _HomePageScreenState(); } class _HomePageScreenState extends State { HomePage? _homePage; bool _error = false; bool _loadExplicitlyRequested = false; final _indicatorKey = GlobalKey(); Future _loadChannel() async { HomePage? _hp; //Fetch channel from api try { if (widget.channel == null) _hp = await deezerAPI.homePage(); else _hp = await deezerAPI.getChannel(widget.channel!.target); } catch (e) { _hp = null; print(e); } if (!mounted) return; if (_hp == null) { //On error setState(() => _error = true); return; } if (_hp!.sections.isEmpty) return; if (widget.cacheable) _hp.save(widget.channel?.target ?? ''); setState(() => _homePage = _hp!); } Future _loadLocalChannel() async { HomePage? _hp = await HomePage.local(widget.channel?.target ?? ''); //print('LOCAL: ${_hp.sections}'); setState(() => _homePage = _hp); } Future _load() async { print("channel: " + (widget.channel?.target ?? "null")); if (widget.homePage != null && widget.homePage!.sections.isNotEmpty) { setState(() => _homePage = widget.homePage); return; } if (!_loadExplicitlyRequested) { if (_homePage == null && widget.cacheable) await _loadLocalChannel(); if (_homePage == null || DateTime.now().difference(_homePage!.lastUpdated) > HomePage.cacheDuration) await _loadChannel(); _loadExplicitlyRequested = true; return; } await _loadChannel(); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _indicatorKey.currentState!.show(); }); } @override Widget build(BuildContext context) { if (_error) return ErrorScreen(); List? sections; if (_homePage != null) { sections = _homePage!.sections!; } return RefreshIndicator( key: _indicatorKey, onRefresh: _load, child: _homePage == null ? const SizedBox.expand(child: SingleChildScrollView()) : ListView.builder( itemBuilder: (context, index) { final section = sections![index]; switch (section.layout) { case HomePageSectionLayout.GRID: return HomePageGridSection(section); case HomePageSectionLayout.ROW: default: return HomepageRowSection(section); } }, itemCount: sections!.length, padding: EdgeInsets.symmetric(horizontal: 8.0), ), ); } } class HomepageRowSection extends StatelessWidget { final HomePageSection section; HomepageRowSection(this.section); @override Widget build(BuildContext context) { return ListTile( title: Text( section.title ?? '', textAlign: TextAlign.left, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900), ), subtitle: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: List.generate(section.items!.length + 1, (j) { //Has more items if (j == section.items!.length) { if (section.hasMore ?? false) { return TextButton( child: Text( 'Show more'.i18n, textAlign: TextAlign.center, style: TextStyle(fontSize: 20.0), ), onPressed: () => Navigator.of(context).pushRoute( builder: (context) => Scaffold( appBar: AppBar(title: Text(section.title!)), body: HomePageScreen( channel: DeezerChannel(target: section.pagePath), ), )), ); } return const SizedBox(); } //Show item HomePageItem item = section.items![j]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: HomePageItemWidget(item), ); }), ), )); } } class HomePageGridSection extends StatelessWidget { final HomePageSection section; HomePageGridSection(this.section); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (section.title != null) Padding( padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0), child: Text( section.title!, textAlign: TextAlign.left, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900), ), ), Wrap( spacing: 4.0, runSpacing: 4.0, alignment: WrapAlignment.spaceEvenly, children: section.items! .map((e) => HomePageItemWidget(e)) .toList(growable: false)), ], ); } } class HomePageItemWidget extends StatelessWidget { final HomePageItem item; HomePageItemWidget(this.item); @override Widget build(BuildContext context) { switch (item.type) { case HomePageItemType.SMARTTRACKLIST: return SmartTrackListTile( item.value, size: (item.value as SmartTrackList).id == 'flow' ? 96.0 : 128.0, onTap: () => playerHelper.playFromSmartTrackList(item.value), ); case HomePageItemType.ALBUM: return AlbumCard( item.value, onTap: () { Navigator.of(context) .pushRoute(builder: (context) => AlbumDetails(item.value)); }, onHold: () { MenuSheet m = MenuSheet(context); m.defaultAlbumMenu(item.value); }, ); case HomePageItemType.ARTIST: return ArtistTile( item.value, onTap: () { Navigator.of(context) .pushRoute(builder: (context) => ArtistDetails(item.value)); }, onHold: () { MenuSheet m = MenuSheet(context); m.defaultArtistMenu(item.value); }, ); case HomePageItemType.PLAYLIST: return PlaylistCardTile( item.value, onTap: () { Navigator.of(context) .pushRoute(builder: (context) => PlaylistDetails(item.value)); }, onHold: () { MenuSheet m = MenuSheet(context); m.defaultPlaylistMenu(item.value); }, ); case HomePageItemType.CHANNEL: return ChannelTile( item.value, onTap: () { Navigator.of(context).pushRoute( builder: (context) => Scaffold( appBar: AppBar(title: Text(item.value.title.toString())), body: HomePageScreen(channel: item.value), )); }, ); case HomePageItemType.SHOW: return ShowCard( item.value, onTap: () { Navigator.of(context) .pushRoute(builder: (context) => ShowScreen(item.value)); }, ); default: return const SizedBox(height: 0, width: 0); } } }