mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-04-04 05:19:38 +00:00
Merge 72333b7794 into e30e84ded1
This commit is contained in:
commit
f64529e482
16 changed files with 159 additions and 68 deletions
|
|
@ -257,6 +257,7 @@ class BookSearchResultMini extends HookConsumerWidget {
|
|||
);
|
||||
},
|
||||
trailing: IconButton(
|
||||
tooltip: 'More options',
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
// TODO: show a menu with options for the book
|
||||
|
|
@ -306,11 +307,12 @@ class SearchResultMiniSection extends HookConsumerWidget {
|
|||
Row(
|
||||
children: [
|
||||
Text(
|
||||
category.toString().split('.').last,
|
||||
category.displayLabel,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'View more ${category.displayLabel}',
|
||||
icon: const Icon(Icons.arrow_forward_ios),
|
||||
onPressed: onTap ?? openSearch,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,23 @@ import 'package:vaani/shared/extensions/model_conversions.dart';
|
|||
|
||||
enum SearchResultCategory { books, authors, series, tags, narrators }
|
||||
|
||||
extension SearchResultCategoryDisplay on SearchResultCategory {
|
||||
String get displayLabel {
|
||||
switch (this) {
|
||||
case SearchResultCategory.books:
|
||||
return 'Books';
|
||||
case SearchResultCategory.authors:
|
||||
return 'Authors';
|
||||
case SearchResultCategory.series:
|
||||
return 'Series';
|
||||
case SearchResultCategory.tags:
|
||||
return 'Tags';
|
||||
case SearchResultCategory.narrators:
|
||||
return 'Narrators';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResultPage extends HookConsumerWidget {
|
||||
const SearchResultPage({
|
||||
super.key,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:vaani/settings/api_settings_provider.dart';
|
|||
import 'package:vaani/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||
import 'package:vaani/shared/utils.dart';
|
||||
import 'package:vaani/shared/widgets/not_implemented.dart';
|
||||
|
||||
class LibraryItemActions extends HookConsumerWidget {
|
||||
const LibraryItemActions({super.key, required this.id});
|
||||
|
|
@ -64,11 +65,15 @@ class LibraryItemActions extends HookConsumerWidget {
|
|||
children: [
|
||||
// read list button
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
tooltip: 'Add to playlist',
|
||||
onPressed: () {
|
||||
showNotImplementedToast(context);
|
||||
},
|
||||
icon: const Icon(Icons.playlist_add_rounded),
|
||||
),
|
||||
// share button
|
||||
IconButton(
|
||||
tooltip: 'Share item',
|
||||
onPressed: () {
|
||||
appLogger.fine('Sharing');
|
||||
var currentServerUrl =
|
||||
|
|
@ -97,6 +102,7 @@ class LibraryItemActions extends HookConsumerWidget {
|
|||
|
||||
// more button
|
||||
IconButton(
|
||||
tooltip: 'More options',
|
||||
onPressed: () {
|
||||
// show the bottom sheet with download history
|
||||
showModalBottomSheet(
|
||||
|
|
@ -215,6 +221,7 @@ class LibItemDownloadButton extends HookConsumerWidget {
|
|||
return isItemDownloading
|
||||
? ItemCurrentlyInDownloadQueue(item: item)
|
||||
: IconButton(
|
||||
tooltip: 'Download item',
|
||||
onPressed: () {
|
||||
appLogger.fine('Pressed download button');
|
||||
|
||||
|
|
@ -280,6 +287,7 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget {
|
|||
final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null;
|
||||
|
||||
return IconButton(
|
||||
tooltip: 'Downloaded item options',
|
||||
onPressed: () {
|
||||
appLogger.fine('Pressed already downloaded button');
|
||||
// manager.openDownloadedFile(item);
|
||||
|
|
|
|||
|
|
@ -137,16 +137,22 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
|||
).colorScheme.primary.withValues(alpha: 0.8),
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/Animation - 1714930099660.json',
|
||||
controller: isPasswordVisibleAnimationController,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: isPasswordVisible.value
|
||||
? 'Hide password'
|
||||
: 'Show password',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/Animation - 1714930099660.json',
|
||||
controller: isPasswordVisibleAnimationController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
|||
child: CircularProgressIndicator(),
|
||||
),
|
||||
ProcessingState.completed => IconButton(
|
||||
tooltip: 'Replay',
|
||||
onPressed: () async {
|
||||
await player.seek(const Duration(seconds: 0));
|
||||
await player.play();
|
||||
|
|
@ -170,6 +171,7 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
|
|||
icon: const Icon(Icons.replay),
|
||||
),
|
||||
ProcessingState.ready => IconButton(
|
||||
tooltip: player.playing ? 'Pause' : 'Play',
|
||||
onPressed: () async {
|
||||
await player.togglePlayPause();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
|||
IconButton(
|
||||
iconSize: 30,
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
tooltip: 'Minimize player',
|
||||
onPressed: () {
|
||||
// minimize the player
|
||||
audioBookMiniplayerController.animateToHeight(
|
||||
|
|
@ -75,6 +76,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
|||
// the cast button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cast),
|
||||
tooltip: 'Cast',
|
||||
onPressed: () {
|
||||
showNotImplementedToast(context);
|
||||
},
|
||||
|
|
@ -108,14 +110,11 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
|||
),
|
||||
child: SizedBox(
|
||||
height: imageSize,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppElementSizes.borderRadiusRegular * earlyPercentage,
|
||||
),
|
||||
child: img,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppElementSizes.borderRadiusRegular * earlyPercentage,
|
||||
),
|
||||
child: img,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -51,20 +51,24 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
|||
horizontal:
|
||||
((availWidth - maxImgSize) / 2) * percentageMiniplayer,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// navigate to item page
|
||||
context.pushNamed(
|
||||
Routes.libraryItem.name,
|
||||
pathParameters: {
|
||||
Routes.libraryItem.pathParamName!:
|
||||
player.book!.libraryItemId,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxImgSize),
|
||||
child: imgWidget,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Open current book details',
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// navigate to item page
|
||||
context.pushNamed(
|
||||
Routes.libraryItem.name,
|
||||
pathParameters: {
|
||||
Routes.libraryItem.pathParamName!:
|
||||
player.book!.libraryItemId,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxImgSize),
|
||||
child: imgWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -112,11 +116,18 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: IconButton(
|
||||
tooltip: 'Rewind 30 seconds',
|
||||
icon: const Icon(
|
||||
Icons.replay_30,
|
||||
size: AppElementSizes.iconSizeSmall,
|
||||
),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
player.seek(
|
||||
player.positionInBook > const Duration(seconds: 30)
|
||||
? player.positionInBook - const Duration(seconds: 30)
|
||||
: Duration.zero,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ class AudiobookPlayerSeekButton extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(audiobookPlayerProvider);
|
||||
return IconButton(
|
||||
tooltip: isForward
|
||||
? 'Seek forward 30 seconds'
|
||||
: 'Seek backward 30 seconds',
|
||||
icon: Icon(
|
||||
isForward ? Icons.forward_30 : Icons.replay_30,
|
||||
size: AppElementSizes.iconSizeSmall,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
return IconButton(
|
||||
tooltip: isForward ? 'Next chapter' : 'Previous chapter',
|
||||
icon: Icon(
|
||||
isForward ? Icons.skip_next : Icons.skip_previous,
|
||||
size: AppElementSizes.iconSizeSmall,
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ class SpeedWheel extends StatelessWidget {
|
|||
// a minus button to decrease the speed
|
||||
if (showIncrementButtons)
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Decrease speed',
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
// animate to index - 1
|
||||
|
|
@ -198,6 +199,7 @@ class SpeedWheel extends StatelessWidget {
|
|||
if (showIncrementButtons)
|
||||
// a plus button to increase the speed
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Increase speed',
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
// animate to index + 1
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import 'package:vaani/shared/extensions/duration_format.dart';
|
|||
class SleepTimerButton extends HookConsumerWidget {
|
||||
const SleepTimerButton({super.key});
|
||||
|
||||
static const _sleepTimerLabel = 'Sleep timer';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sleepTimer = ref.watch(sleepTimerProvider);
|
||||
|
|
@ -23,40 +25,44 @@ class SleepTimerButton extends HookConsumerWidget {
|
|||
// if sleep timer is not active, show the button with the sleep timer icon
|
||||
// if the sleep timer is active, show the remaining time in a pill shaped container
|
||||
return Tooltip(
|
||||
message: 'Sleep Timer',
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
appLogger.fine('Sleep Timer button pressed');
|
||||
pendingPlayerModals++;
|
||||
// show the sleep timer dialog
|
||||
await showModalBottomSheet<Duration?>(
|
||||
context: context,
|
||||
barrierLabel: 'Sleep Timer',
|
||||
builder: (context) {
|
||||
return SleepTimerBottomSheet(
|
||||
onDurationSelected: (duration) {
|
||||
durationState.value = duration;
|
||||
// ref
|
||||
// .read(sleepTimerProvider.notifier)
|
||||
// .setTimer(duration, notifyListeners: false);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
pendingPlayerModals--;
|
||||
ref.read(sleepTimerProvider.notifier).setTimer(durationState.value);
|
||||
appLogger.fine(
|
||||
'Sleep Timer dialog closed with ${durationState.value}',
|
||||
);
|
||||
},
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: sleepTimer == null
|
||||
? Icon(
|
||||
Symbols.bedtime,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
)
|
||||
: RemainingSleepTimeDisplay(timer: sleepTimer),
|
||||
message: _sleepTimerLabel,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: _sleepTimerLabel,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
appLogger.fine('Sleep Timer button pressed');
|
||||
pendingPlayerModals++;
|
||||
// show the sleep timer dialog
|
||||
await showModalBottomSheet<Duration?>(
|
||||
context: context,
|
||||
barrierLabel: 'Sleep Timer',
|
||||
builder: (context) {
|
||||
return SleepTimerBottomSheet(
|
||||
onDurationSelected: (duration) {
|
||||
durationState.value = duration;
|
||||
// ref
|
||||
// .read(sleepTimerProvider.notifier)
|
||||
// .setTimer(duration, notifyListeners: false);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
pendingPlayerModals--;
|
||||
ref.read(sleepTimerProvider.notifier).setTimer(durationState.value);
|
||||
appLogger.fine(
|
||||
'Sleep Timer dialog closed with ${durationState.value}',
|
||||
);
|
||||
},
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: sleepTimer == null
|
||||
? Icon(
|
||||
Symbols.bedtime,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
)
|
||||
: RemainingSleepTimeDisplay(timer: sleepTimer),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -256,6 +262,7 @@ class SleepTimerWheel extends StatelessWidget {
|
|||
// a minus button to decrease the speed
|
||||
if (showIncrementButtons)
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Decrease sleep timer',
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
// animate to index - 1
|
||||
|
|
@ -294,6 +301,7 @@ class SleepTimerWheel extends StatelessWidget {
|
|||
if (showIncrementButtons)
|
||||
// a plus button to increase the speed
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Increase sleep timer',
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
// animate to index + 1
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ class AvailableUserTile extends HookConsumerWidget {
|
|||
context.goNamed(Routes.home.name);
|
||||
},
|
||||
trailing: IconButton(
|
||||
tooltip: 'Remove user',
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ class RestoreDialogue extends HookConsumerWidget {
|
|||
hintText: 'Paste the backup here',
|
||||
// clear button
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Clear backup',
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
settingsInputController.clear();
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
|||
decoration: InputDecoration(
|
||||
helper: const Text('Select a field below to insert it'),
|
||||
suffix: IconButton(
|
||||
tooltip: 'Clear title',
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ class ShakeForceSelector extends HookConsumerWidget {
|
|||
decoration: InputDecoration(
|
||||
// clear button
|
||||
suffix: IconButton(
|
||||
tooltip: 'Clear threshold',
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
|
|
|
|||
|
|
@ -206,6 +206,27 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
|||
/// the id of the library item of the book
|
||||
final String libraryItemId;
|
||||
|
||||
String getPlayTooltip({
|
||||
required bool isCurrentBookSetInPlayer,
|
||||
required bool isPlayingThisBook,
|
||||
required bool isBookCompleted,
|
||||
required bool hasProgress,
|
||||
}) {
|
||||
if (isCurrentBookSetInPlayer) {
|
||||
return isPlayingThisBook ? 'Pause' : 'Resume';
|
||||
}
|
||||
|
||||
if (isBookCompleted) {
|
||||
return 'Listen again';
|
||||
}
|
||||
|
||||
if (hasProgress) {
|
||||
return 'Continue listening';
|
||||
}
|
||||
|
||||
return 'Start listening';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final me = ref.watch(meProvider);
|
||||
|
|
@ -218,6 +239,12 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
|||
(element) => element.libraryItemId == libraryItemId,
|
||||
);
|
||||
final isBookCompleted = userProgress?.isFinished ?? false;
|
||||
final playTooltip = getPlayTooltip(
|
||||
isCurrentBookSetInPlayer: isCurrentBookSetInPlayer,
|
||||
isPlayingThisBook: isPlayingThisBook,
|
||||
isBookCompleted: isBookCompleted,
|
||||
hasProgress: userProgress?.progress != null,
|
||||
);
|
||||
|
||||
const size = 40.0;
|
||||
|
||||
|
|
@ -268,6 +295,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
|||
|
||||
// the play button
|
||||
IconButton(
|
||||
tooltip: playTooltip,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue