freezer/lib/ui/home_screen.dart
Pato05 2862c9ec05
remove browser login for desktop
restore translations functionality
make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available
exit app if opening cache or settings fails (another instance running)
remove draggable_scrollbar and use builtin widget instead
fix email login
better way to manage lyrics (less updates and lookups in the lyrics List)
fix player_screen on mobile (too big -> just average :))
right click: use TapUp events instead
desktop: show context menu on triple dots button also
avoid showing connection error if the homepage is cached and available offline
i'm probably forgetting something idk
2023-10-25 00:32:28 +02:00

431 lines
13 KiB
Dart

import 'package:flutter/gestures.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/main.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) {
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();
});
}
Widget getSectionChild(HomePageSection section) {
switch (section.layout) {
case HomePageSectionLayout.GRID:
return HomePageGridSection(section);
case HomePageSectionLayout.ROW:
default:
return HomepageRowSection(section);
}
}
@override
Widget build(BuildContext context) {
if (_error) return const ErrorScreen();
List<HomePageSection>? sections;
if (_homePage != null) {
sections = _homePage!.sections;
}
final actualScrollConfiguration = ScrollConfiguration.of(context);
return ScrollConfiguration(
behavior: actualScrollConfiguration.copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.stylus,
PointerDeviceKind.touch,
PointerDeviceKind.trackpad
},
),
child: RefreshIndicator(
key: _indicatorKey,
onRefresh: _load,
child: _homePage == null
? const SizedBox.expand(child: SingleChildScrollView())
: ListView.builder(
itemBuilder: (context, index) {
return Padding(
padding: index == 0
? EdgeInsets.zero
: const EdgeInsets.only(top: 16.0),
child: getSectionChild(sections![index]));
},
itemCount: sections!.length,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
),
),
);
}
}
class HomepageRowSection extends StatefulWidget {
final HomePageSection section;
const HomepageRowSection(this.section, {super.key});
@override
State<HomepageRowSection> createState() => _HomepageRowSectionState();
}
class _HomepageRowSectionState extends State<HomepageRowSection> {
final _controller = ScrollController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
widget.section.title ?? '',
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900),
),
subtitle: Scrollbar(
controller: _controller,
thickness: MainScreen.of(context).isDesktop ? null : 1.0,
child: SingleChildScrollView(
controller: _controller,
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(widget.section.items!.length + 1, (j) {
//Has more items
if (j == widget.section.items!.length) {
if (widget.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: widget.section.title!,
channel:
DeezerChannel(target: widget.section.pagePath),
),
),
);
}
return const SizedBox();
}
//Show item
HomePageItem item = widget.section.items![j];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: HomePageItemWidget(item),
);
}),
),
),
));
}
}
class HomePageGridSection extends StatelessWidget {
final HomePageSection section;
final double horizontalSpacing;
final double verticalSpacing;
const HomePageGridSection(
this.section, {
this.horizontalSpacing = 6.0,
this.verticalSpacing = 6.0,
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),
),
),
LayoutBuilder(builder: (context, constraints) {
final amountThatCanFit =
constraints.maxWidth ~/ (150.0 + horizontalSpacing);
final widthOfEach =
constraints.maxWidth / amountThatCanFit - horizontalSpacing;
return Wrap(
spacing: horizontalSpacing,
runSpacing: verticalSpacing,
alignment: WrapAlignment.start,
children: section.items!
.map((e) => SizedBox(
width: widthOfEach,
child: 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);
}
}
}