chore: run dart format
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions

This commit is contained in:
Dr.Blank 2026-01-10 16:51:05 +05:30
parent a520136e01
commit e23c0b6c5f
No known key found for this signature in database
GPG key ID: BA5F87FF0560C57B
84 changed files with 1565 additions and 1945 deletions

View file

@ -26,10 +26,7 @@ import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/utils.dart';
class LibraryItemActions extends HookConsumerWidget {
const LibraryItemActions({
super.key,
required this.id,
});
const LibraryItemActions({super.key, required this.id});
final String id;
@ -68,9 +65,7 @@ class LibraryItemActions extends HookConsumerWidget {
// read list button
IconButton(
onPressed: () {},
icon: const Icon(
Icons.playlist_add_rounded,
),
icon: const Icon(Icons.playlist_add_rounded),
),
// share button
IconButton(
@ -79,8 +74,9 @@ class LibraryItemActions extends HookConsumerWidget {
var currentServerUrl =
apiSettings.activeServer!.serverUrl;
if (!currentServerUrl.hasScheme) {
currentServerUrl =
Uri.https(currentServerUrl.toString());
currentServerUrl = Uri.https(
currentServerUrl.toString(),
);
}
handleLaunchUrl(
Uri.parse(
@ -140,7 +136,8 @@ class LibraryItemActions extends HookConsumerWidget {
.database
.deleteRecordWithId(
record
.task.taskId,
.task
.taskId,
);
Navigator.pop(context);
},
@ -161,8 +158,8 @@ class LibraryItemActions extends HookConsumerWidget {
// open the file location
final didOpen =
await FileDownloader().openFile(
task: record.task,
);
task: record.task,
);
if (!didOpen) {
appLogger.warning(
@ -182,16 +179,13 @@ class LibraryItemActions extends HookConsumerWidget {
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
error: (error, stackTrace) =>
Center(child: Text('Error: $error')),
);
},
);
},
icon: const Icon(
Icons.more_vert_rounded,
),
icon: const Icon(Icons.more_vert_rounded),
),
],
),
@ -206,10 +200,7 @@ class LibraryItemActions extends HookConsumerWidget {
}
class LibItemDownloadButton extends HookConsumerWidget {
const LibItemDownloadButton({
super.key,
required this.item,
});
const LibItemDownloadButton({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@ -222,9 +213,7 @@ class LibItemDownloadButton extends HookConsumerWidget {
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
return isItemDownloading
? ItemCurrentlyInDownloadQueue(
item: item,
)
? ItemCurrentlyInDownloadQueue(item: item)
: IconButton(
onPressed: () {
appLogger.fine('Pressed download button');
@ -233,18 +222,13 @@ class LibItemDownloadButton extends HookConsumerWidget {
.read(downloadManagerProvider.notifier)
.queueAudioBookDownload(item);
},
icon: const Icon(
Icons.download_rounded,
),
icon: const Icon(Icons.download_rounded),
);
}
}
class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
const ItemCurrentlyInDownloadQueue({
super.key,
required this.item,
});
const ItemCurrentlyInDownloadQueue({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@ -263,17 +247,12 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
return Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: progress,
strokeWidth: 2,
),
CircularProgressIndicator(value: progress, strokeWidth: 2),
const Icon(
Icons.download,
// color: Theme.of(context).progressIndicatorTheme.color,
)
.animate(
onPlay: (controller) => controller.repeat(),
Icons.download,
// color: Theme.of(context).progressIndicatorTheme.color,
)
.animate(onPlay: (controller) => controller.repeat())
.fade(
duration: shimmerDuration,
end: 1,
@ -292,10 +271,7 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
}
class AlreadyItemDownloadedButton extends HookConsumerWidget {
const AlreadyItemDownloadedButton({
super.key,
required this.item,
});
const AlreadyItemDownloadedButton({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@ -317,25 +293,18 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget {
top: 8.0,
bottom: (isBookPlaying ? playerMinHeight : 0) + 8,
),
child: DownloadSheet(
item: item,
),
child: DownloadSheet(item: item),
);
},
);
},
icon: const Icon(
Icons.download_done_rounded,
),
icon: const Icon(Icons.download_done_rounded),
);
}
}
class DownloadSheet extends HookConsumerWidget {
const DownloadSheet({
super.key,
required this.item,
});
const DownloadSheet({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@ -367,9 +336,7 @@ class DownloadSheet extends HookConsumerWidget {
// ),
ListTile(
title: const Text('Delete'),
leading: const Icon(
Icons.delete_rounded,
),
leading: const Icon(Icons.delete_rounded),
onTap: () async {
// show the delete dialog
final wasDeleted = await showDialog<bool>(
@ -387,9 +354,7 @@ class DownloadSheet extends HookConsumerWidget {
// delete the file
ref
.read(downloadManagerProvider.notifier)
.deleteDownloadedItem(
item,
);
.deleteDownloadedItem(item);
GoRouter.of(context).pop(true);
},
child: const Text('Yes'),
@ -409,11 +374,7 @@ class DownloadSheet extends HookConsumerWidget {
appLogger.fine('Deleted ${item.media.metadata.title}');
GoRouter.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Deleted ${item.media.metadata.title}',
),
),
SnackBar(content: Text('Deleted ${item.media.metadata.title}')),
);
}
},
@ -424,9 +385,7 @@ class DownloadSheet extends HookConsumerWidget {
}
class _LibraryItemPlayButton extends HookConsumerWidget {
const _LibraryItemPlayButton({
required this.item,
});
const _LibraryItemPlayButton({required this.item});
final shelfsdk.LibraryItemExpanded item;
@ -477,9 +436,7 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
),
label: Text(getPlayDisplayText()),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
),
);
}
@ -502,11 +459,11 @@ class DynamicItemPlayIcon extends StatelessWidget {
return Icon(
isCurrentBookSetInPlayer
? isPlayingThisBook
? Icons.pause_rounded
: Icons.play_arrow_rounded
? Icons.pause_rounded
: Icons.play_arrow_rounded
: isBookCompleted
? Icons.replay_rounded
: Icons.play_arrow_rounded,
? Icons.replay_rounded
: Icons.play_arrow_rounded,
);
}
}
@ -529,8 +486,9 @@ Future<void> libraryItemPlayButtonOnPressed({
appLogger.info('Setting the book ${book.libraryItemId}');
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
final downloadManager = ref.watch(simpleDownloadManagerProvider);
final libItem =
await ref.read(libraryItemProvider(book.libraryItemId).future);
final libItem = await ref.read(
libraryItemProvider(book.libraryItemId).future,
);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
setSourceFuture = player.setSourceAudiobook(
book,
@ -546,8 +504,9 @@ Future<void> libraryItemPlayButtonOnPressed({
}
}
// set the volume as this is the first time playing and dismissing causes the volume to go to 0
var bookPlayerSettings =
ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings;
var bookPlayerSettings = ref
.read(bookSettingsProvider(book.libraryItemId))
.playerSettings;
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
var configurePlayerForEveryBook =
@ -559,14 +518,14 @@ Future<void> libraryItemPlayButtonOnPressed({
player.setVolume(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultVolume ??
appPlayerSettings.preferredDefaultVolume
appPlayerSettings.preferredDefaultVolume
: appPlayerSettings.preferredDefaultVolume,
),
// set the speed
player.setSpeed(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultSpeed ??
appPlayerSettings.preferredDefaultSpeed
appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed,
),
]);

View file

@ -42,14 +42,13 @@ class LibraryItemHeroSection extends HookConsumerWidget {
child: Column(
children: [
Hero(
tag: HeroTagPrefixes.bookCover +
tag:
HeroTagPrefixes.bookCover +
itemId +
(extraMap?.heroTagSuffix ?? ''),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _BookCover(
itemId: itemId,
),
child: _BookCover(itemId: itemId),
),
),
// a progress bar
@ -59,9 +58,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
right: 8.0,
left: 8.0,
),
child: _LibraryItemProgressIndicator(
id: itemId,
),
child: _LibraryItemProgressIndicator(id: itemId),
),
],
),
@ -77,10 +74,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
}
class _BookDetails extends HookConsumerWidget {
const _BookDetails({
required this.id,
this.extraMap,
});
const _BookDetails({required this.id, this.extraMap});
final String id;
final LibraryItemExtras? extraMap;
@ -99,10 +93,7 @@ class _BookDetails extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_BookTitle(
extraMap: extraMap,
itemBookMetadata: itemBookMetadata,
),
_BookTitle(extraMap: extraMap, itemBookMetadata: itemBookMetadata),
Container(
margin: const EdgeInsets.symmetric(vertical: 16),
child: Column(
@ -134,9 +125,7 @@ class _BookDetails extends HookConsumerWidget {
}
class _LibraryItemProgressIndicator extends HookConsumerWidget {
const _LibraryItemProgressIndicator({
required this.id,
});
const _LibraryItemProgressIndicator({required this.id});
final String id;
@ -157,13 +146,15 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
Duration remainingTime;
if (player.book?.libraryItemId == libraryItem.id) {
// final positionStream = useStream(player.slowPositionStream);
progress = (player.positionInBook).inSeconds /
progress =
(player.positionInBook).inSeconds /
libraryItem.media.asBookExpanded.duration.inSeconds;
remainingTime =
libraryItem.media.asBookExpanded.duration - player.positionInBook;
} else {
progress = mediaProgress?.progress ?? 0;
remainingTime = (libraryItem.media.asBookExpanded.duration -
remainingTime =
(libraryItem.media.asBookExpanded.duration -
mediaProgress!.currentTime);
}
@ -190,20 +181,17 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
semanticsLabel: 'Book progress',
semanticsValue: '${progressInPercent.toStringAsFixed(2)}%',
),
const SizedBox.square(
dimension: 4.0,
),
const SizedBox.square(dimension: 4.0),
// time remaining
Text(
// only show 2 decimal places
'${remainingTime.smartBinaryFormat} left',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.75),
),
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.75),
),
),
],
),
@ -212,10 +200,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
}
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
const _HeroSectionSubLabelWithIcon({
required this.icon,
required this.text,
});
const _HeroSectionSubLabelWithIcon({required this.icon, required this.text});
final IconData icon;
final Widget text;
@ -225,8 +210,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
final themeData = Theme.of(context);
final useFontAwesome =
icon.runtimeType == FontAwesomeIcons.book.runtimeType;
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
final useMaterialThemeOnItemPage = ref
.watch(appSettingsProvider)
.themeSettings
.useMaterialThemeOnItemPage;
final color = useMaterialThemeOnItemPage
? themeData.colorScheme.primary
: themeData.colorScheme.onSurface.withValues(alpha: 0.75);
@ -237,20 +224,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
Container(
margin: const EdgeInsets.only(right: 8, top: 2),
child: useFontAwesome
? FaIcon(
icon,
size: 16,
color: color,
)
: Icon(
icon,
size: 16,
color: color,
),
),
Expanded(
child: text,
? FaIcon(icon, size: 16, color: color)
: Icon(icon, size: 16, color: color),
),
Expanded(child: text),
],
),
);
@ -338,9 +315,7 @@ class _BookNarrators extends StatelessWidget {
}
class _BookCover extends HookConsumerWidget {
const _BookCover({
required this.itemId,
});
const _BookCover({required this.itemId});
final String itemId;
@ -358,7 +333,8 @@ class _BookCover extends HookConsumerWidget {
themeOfLibraryItemProvider(
itemId,
brightness: Theme.of(context).brightness,
highContrast: themeSettings.highContrast ||
highContrast:
themeSettings.highContrast ||
MediaQuery.of(context).highContrast,
),
)
@ -391,15 +367,10 @@ class _BookCover extends HookConsumerWidget {
return const Icon(Icons.error);
}
return Image.memory(
image,
fit: BoxFit.cover,
);
return Image.memory(image, fit: BoxFit.cover);
},
loading: () {
return const Center(
child: BookCoverSkeleton(),
);
return const Center(child: BookCoverSkeleton());
},
error: (error, stack) {
return const Center(child: Icon(Icons.error));
@ -411,10 +382,7 @@ class _BookCover extends HookConsumerWidget {
}
class _BookTitle extends StatelessWidget {
const _BookTitle({
required this.extraMap,
required this.itemBookMetadata,
});
const _BookTitle({required this.extraMap, required this.itemBookMetadata});
final LibraryItemExtras? extraMap;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
@ -426,7 +394,8 @@ class _BookTitle extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: HeroTagPrefixes.bookTitle +
tag:
HeroTagPrefixes.bookTitle +
// itemId +
(extraMap?.heroTagSuffix ?? ''),
child: Text(

View file

@ -4,10 +4,7 @@ import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
class LibraryItemMetadata extends HookConsumerWidget {
const LibraryItemMetadata({
super.key,
required this.id,
});
const LibraryItemMetadata({super.key, required this.id});
final String id;
@ -72,7 +69,8 @@ class LibraryItemMetadata extends HookConsumerWidget {
),
_MetadataItem(
title: 'Published',
value: itemBookMetadata?.publishedDate ??
value:
itemBookMetadata?.publishedDate ??
itemBookMetadata?.publishedYear ??
'Unknown',
),
@ -87,22 +85,18 @@ class LibraryItemMetadata extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// alternate between metadata and vertical divider
children: List.generate(
children.length * 2 - 1,
(index) {
if (index.isEven) {
return children[index ~/ 2];
}
return VerticalDivider(
indent: 6,
endIndent: 6,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
);
},
),
children: List.generate(children.length * 2 - 1, (index) {
if (index.isEven) {
return children[index ~/ 2];
}
return VerticalDivider(
indent: 6,
endIndent: 6,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
);
}),
),
),
);
@ -111,10 +105,7 @@ class LibraryItemMetadata extends HookConsumerWidget {
/// key-value pair to display as column
class _MetadataItem extends StatelessWidget {
const _MetadataItem({
required this.title,
required this.value,
});
const _MetadataItem({required this.title, required this.value});
final String title;
final String value;

View file

@ -16,49 +16,43 @@ import 'library_item_hero_section.dart';
import 'library_item_metadata.dart';
class LibraryItemPage extends HookConsumerWidget {
const LibraryItemPage({
super.key,
required this.itemId,
this.extra,
});
const LibraryItemPage({super.key, required this.itemId, this.extra});
final String itemId;
final Object? extra;
static const double _showFabThreshold = 300.0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final additionalItemData =
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
final additionalItemData = extra is LibraryItemExtras
? extra as LibraryItemExtras
: null;
final scrollController = useScrollController();
final showFab = useState(false);
// Effect to listen to scroll changes and update FAB visibility
useEffect(
() {
void listener() {
if (!scrollController.hasClients) {
return; // Ensure controller is attached
}
final shouldShow = scrollController.offset > _showFabThreshold;
// Update state only if it changes and widget is still mounted
if (showFab.value != shouldShow && context.mounted) {
showFab.value = shouldShow;
}
useEffect(() {
void listener() {
if (!scrollController.hasClients) {
return; // Ensure controller is attached
}
final shouldShow = scrollController.offset > _showFabThreshold;
// Update state only if it changes and widget is still mounted
if (showFab.value != shouldShow && context.mounted) {
showFab.value = shouldShow;
}
}
scrollController.addListener(listener);
// Initial check in case the view starts scrolled (less likely but safe)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients && context.mounted) {
listener();
}
});
scrollController.addListener(listener);
// Initial check in case the view starts scrolled (less likely but safe)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients && context.mounted) {
listener();
}
});
// Cleanup: remove the listener when the widget is disposed
return () => scrollController.removeListener(listener);
},
[scrollController],
); // Re-run effect if scrollController changes
// Cleanup: remove the listener when the widget is disposed
return () => scrollController.removeListener(listener);
}, [scrollController]); // Re-run effect if scrollController changes
// --- FAB Scroll-to-Top Logic ---
void scrollToTop() {
@ -82,10 +76,7 @@ class LibraryItemPage extends HookConsumerWidget {
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),
child: FadeTransition(opacity: animation, child: child),
);
},
child: showFab.value
@ -96,9 +87,7 @@ class LibraryItemPage extends HookConsumerWidget {
tooltip: 'Scroll to top',
child: const Icon(Icons.arrow_upward),
)
: const SizedBox.shrink(
key: ValueKey('fab-empty'),
),
: const SizedBox.shrink(key: ValueKey('fab-empty')),
),
body: CustomScrollView(
controller: scrollController,
@ -115,17 +104,11 @@ class LibraryItemPage extends HookConsumerWidget {
),
),
// a horizontal display with dividers of metadata
SliverToBoxAdapter(
child: LibraryItemMetadata(id: itemId),
),
SliverToBoxAdapter(child: LibraryItemMetadata(id: itemId)),
// a row of actions like play, download, share, etc
SliverToBoxAdapter(
child: LibraryItemActions(id: itemId),
),
SliverToBoxAdapter(child: LibraryItemActions(id: itemId)),
// a expandable section for book description
SliverToBoxAdapter(
child: LibraryItemDescription(id: itemId),
),
SliverToBoxAdapter(child: LibraryItemDescription(id: itemId)),
// a padding at the bottom to make sure the last item is not hidden by mini player
const SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
],
@ -137,10 +120,7 @@ class LibraryItemPage extends HookConsumerWidget {
}
class LibraryItemDescription extends HookConsumerWidget {
const LibraryItemDescription({
super.key,
required this.id,
});
const LibraryItemDescription({super.key, required this.id});
final String id;
@override
@ -160,16 +140,21 @@ class LibraryItemDescription extends HookConsumerWidget {
double calculateWidth(
BuildContext context,
BoxConstraints constraints, {
/// width ratio of the cover image to the available width
double widthRatio = 0.4,
/// height ratio of the cover image to the available height
double maxHeightToUse = 0.25,
}) {
final availHeight =
min(constraints.maxHeight, MediaQuery.of(context).size.height);
final availWidth =
min(constraints.maxWidth, MediaQuery.of(context).size.width);
final availHeight = min(
constraints.maxHeight,
MediaQuery.of(context).size.height,
);
final availWidth = min(
constraints.maxWidth,
MediaQuery.of(context).size.width,
);
// make the width widthRatio of the available width
var width = availWidth * widthRatio;

View file

@ -21,28 +21,26 @@ class LibraryItemSliverAppBar extends HookConsumerWidget {
final showTitle = useState(false);
useEffect(
() {
void listener() {
final shouldShow = scrollController.hasClients &&
scrollController.offset > _showTitleThreshold;
if (showTitle.value != shouldShow) {
showTitle.value = shouldShow;
}
useEffect(() {
void listener() {
final shouldShow =
scrollController.hasClients &&
scrollController.offset > _showTitleThreshold;
if (showTitle.value != shouldShow) {
showTitle.value = shouldShow;
}
}
scrollController.addListener(listener);
// Trigger listener once initially in case the view starts scrolled
// (though unlikely for this specific use case, it's good practice)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
listener();
}
});
return () => scrollController.removeListener(listener);
},
[scrollController],
);
scrollController.addListener(listener);
// Trigger listener once initially in case the view starts scrolled
// (though unlikely for this specific use case, it's good practice)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
listener();
}
});
return () => scrollController.removeListener(listener);
}, [scrollController]);
return SliverAppBar(
elevation: 0,