refactor: add genre carousel buttons and indicators

This commit is contained in:
Kingkor Roy Tirtho 2025-01-06 21:33:26 +06:00
parent bf94a490bb
commit 46852545a9
2 changed files with 274 additions and 185 deletions

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -36,11 +38,31 @@ class HomeGenresSection extends HookConsumerWidget {
],
[mediaQuery.mdAndDown, categoriesQuery.asData?.value],
);
final controller = useMemoized(() => CarouselController(), []);
final interactedRef = useRef(false);
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: Padding(
useEffect(() {
int times = 0;
Timer.periodic(
const Duration(seconds: 5),
(timer) {
if (times > 5 || interactedRef.value) {
timer.cancel();
return;
}
controller.animateNext(
const Duration(seconds: 2),
);
times++;
},
);
return controller.dispose;
}, []);
return SliverList.list(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -64,12 +86,13 @@ class HomeGenresSection extends HookConsumerWidget {
],
),
),
),
const SliverGap(8),
SliverToBoxAdapter(
child: SizedBox(
const Gap(8),
Stack(
children: [
SizedBox(
height: 280 * theme.scaling,
child: Carousel(
controller: controller,
transition: const CarouselTransition.sliding(gap: 24),
sizeConstraint: CarouselSizeConstraint.fixed(
mediaQuery.mdAndUp
@ -77,8 +100,6 @@ class HomeGenresSection extends HookConsumerWidget {
: mediaQuery.width * .95,
),
itemCount: categories.length,
autoplaySpeed: const Duration(seconds: 2),
duration: const Duration(seconds: 5),
pauseOnHover: true,
direction: Axis.horizontal,
itemBuilder: (context, index) {
@ -156,14 +177,15 @@ class HomeGenresSection extends HookConsumerWidget {
borderRadius: theme.borderRadiusMd,
surfaceBlur: theme.surfaceBlur,
child: Button(
style:
ButtonVariance.secondary.copyWith(
padding: (context, states, value) =>
style: ButtonVariance.secondary
.copyWith(
padding:
(context, states, value) =>
const EdgeInsets.all(8),
decoration:
(context, states, value) {
final decoration = ButtonVariance
.secondary
final decoration =
ButtonVariance.secondary
.decoration(
context, states)
as BoxDecoration;
@ -211,9 +233,11 @@ class HomeGenresSection extends HookConsumerWidget {
Text(
playlist.name!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
overflow:
TextOverflow.ellipsis,
).semiBold().small(),
if (playlist.description != null)
if (playlist.description !=
null)
Text(
playlist.description
?.unescapeHtml()
@ -240,6 +264,72 @@ class HomeGenresSection extends HookConsumerWidget {
},
),
),
Positioned(
left: 0,
child: Container(
height: 280 * theme.scaling,
width: (mediaQuery.mdAndUp ? 80 : 50) * theme.scaling,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
theme.colorScheme.background.withAlpha(255),
theme.colorScheme.background.withAlpha(0),
],
),
),
alignment: Alignment.center,
child: IconButton.ghost(
size:
mediaQuery.mdAndUp ? ButtonSize.normal : ButtonSize.small,
icon: const Icon(SpotubeIcons.angleLeft),
onPressed: () {
controller.animatePrevious(
const Duration(seconds: 1),
);
interactedRef.value = true;
},
),
),
),
Positioned(
right: 0,
child: Container(
height: 280 * theme.scaling,
width: (mediaQuery.mdAndUp ? 80 : 50) * theme.scaling,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
theme.colorScheme.background.withAlpha(0),
theme.colorScheme.background.withAlpha(255),
],
),
),
alignment: Alignment.center,
child: IconButton.ghost(
size:
mediaQuery.mdAndUp ? ButtonSize.normal : ButtonSize.small,
icon: const Icon(SpotubeIcons.angleRight),
onPressed: () {
controller.animateNext(
const Duration(seconds: 1),
);
interactedRef.value = true;
},
),
),
),
],
),
const Gap(8),
Center(
child: CarouselDotIndicator(
itemCount: categories.length,
controller: controller,
),
),
],
);

View File

@ -1,10 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:path/path.dart' as path;
import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
@ -22,7 +21,7 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final ThemeData(:typography, :colorScheme) = Theme.of(context);
final files = useState<List<File>>([]);
final filesExported = useState<int>(0);
@ -31,7 +30,7 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
final stream = cacheDir.list().where(
(event) =>
event is File &&
codecs.contains(extension(event.path).replaceAll(".", "")),
codecs.contains(path.extension(event.path).replaceAll(".", "")),
);
stream.listen(
@ -76,8 +75,8 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
),
TextSpan(
text: "\n${exportDir.path}?",
style: textTheme.labelMedium!.copyWith(
color: colorScheme.secondary,
style: typography.small.copyWith(
color: colorScheme.mutedForeground,
),
),
],
@ -102,7 +101,7 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
),
),
actions: [
TextButton(
Button.outline(
onPressed: isExportInProgress
? null
: () {
@ -110,14 +109,14 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
},
child: Text(context.l10n.cancel),
),
TextButton(
Button.primary(
onPressed: isExportInProgress
? null
: () async {
for (final file in files.value) {
try {
final destinationFile = File(
join(exportDir.path, basename(file.path)),
path.join(exportDir.path, path.basename(file.path)),
);
if (await destinationFile.exists()) {