use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
373 lines
11 KiB
Dart
373 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:freezer/api/player/audio_handler.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';
|
|
|
|
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 {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Scaffold(
|
|
body: NestedScrollView(
|
|
floatHeaderSlivers: true,
|
|
headerSliverBuilder: (context, _) => [
|
|
SliverPersistentHeader(
|
|
delegate: _SearchHeaderDelegate(), floating: true)
|
|
],
|
|
body: const HomePageWidget(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: <Widget>[
|
|
Image.asset('assets/icon.png', width: 64, height: 64),
|
|
const Text(
|
|
'freezer',
|
|
style: TextStyle(fontSize: 56, fontWeight: FontWeight.w900),
|
|
)
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomePageScreen extends StatelessWidget {
|
|
final String title;
|
|
final HomePage? homePage;
|
|
final bool cacheable;
|
|
final DeezerChannel channel;
|
|
|
|
const HomePageScreen({
|
|
super.key,
|
|
required this.title,
|
|
required this.channel,
|
|
this.homePage,
|
|
this.cacheable = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(title)),
|
|
body: HomePageWidget(
|
|
homePage: homePage,
|
|
channel: channel,
|
|
cacheable: cacheable,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomePageWidget extends StatefulWidget {
|
|
final HomePage? homePage;
|
|
final bool cacheable;
|
|
final DeezerChannel? channel;
|
|
const HomePageWidget(
|
|
{this.homePage, this.channel, this.cacheable = false, super.key});
|
|
|
|
@override
|
|
State<HomePageWidget> createState() => _HomePageWidgetState();
|
|
}
|
|
|
|
class _HomePageWidgetState extends State<HomePageWidget> {
|
|
HomePage? _homePage;
|
|
bool _error = false;
|
|
bool _loadExplicitlyRequested = false;
|
|
final _indicatorKey = GlobalKey<RefreshIndicatorState>();
|
|
|
|
Future<void> _loadChannel() async {
|
|
HomePage? homePage;
|
|
//Fetch channel from api
|
|
try {
|
|
if (widget.channel == null) {
|
|
homePage = await deezerAPI.homePage();
|
|
} else {
|
|
homePage = await deezerAPI.getChannel(widget.channel!.target);
|
|
}
|
|
} catch (e) {
|
|
homePage = null;
|
|
}
|
|
if (!mounted) return;
|
|
if (homePage == null) {
|
|
//On error
|
|
setState(() => _error = true);
|
|
return;
|
|
}
|
|
if (homePage.sections.isEmpty) return;
|
|
if (widget.cacheable) homePage.save(widget.channel?.target ?? '');
|
|
setState(() => _homePage = homePage!);
|
|
}
|
|
|
|
Future<void> _loadLocalChannel() async {
|
|
final HomePage? homePage =
|
|
await HomePage.local(widget.channel?.target ?? '');
|
|
//print('LOCAL: ${_hp.sections}');
|
|
if (homePage != null) setState(() => _homePage = homePage);
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
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 const ErrorScreen();
|
|
List<HomePageSection>? 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: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomepageRowSection extends StatelessWidget {
|
|
final HomePageSection section;
|
|
const HomepageRowSection(this.section, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
title: Text(
|
|
section.title ?? '',
|
|
textAlign: TextAlign.left,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const 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: const TextStyle(fontSize: 20.0),
|
|
),
|
|
onPressed: () => Navigator.of(context).pushRoute(
|
|
builder: (context) => HomePageScreen(
|
|
title: section.title!,
|
|
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;
|
|
const HomePageGridSection(this.section, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (section.title != null)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
|
|
child: Text(
|
|
section.title!,
|
|
textAlign: TextAlign.left,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style:
|
|
const 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;
|
|
const HomePageItemWidget(this.item, {super.key});
|
|
|
|
@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));
|
|
},
|
|
onSecondary: (details) {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultArtistMenu(item.value, details: details);
|
|
},
|
|
);
|
|
case HomePageItemType.PLAYLIST:
|
|
return PlaylistCardTile(
|
|
item.value,
|
|
onTap: () {
|
|
Navigator.of(context)
|
|
.pushRoute(builder: (context) => PlaylistDetails(item.value));
|
|
},
|
|
onSecondary: (details) {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultPlaylistMenu(item.value, details: details);
|
|
},
|
|
);
|
|
case HomePageItemType.CHANNEL:
|
|
return ChannelTile(
|
|
item.value,
|
|
onTap: () {
|
|
Navigator.of(context).pushRoute(
|
|
builder: (context) => HomePageScreen(
|
|
channel: item.value,
|
|
title: item.value.title.toString(),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
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);
|
|
}
|
|
}
|
|
}
|