mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-09 04:29:29 +00:00
feat: Add miniplayer
This commit is contained in:
parent
610d9a2aa0
commit
7f5309d10a
25 changed files with 355 additions and 29 deletions
107
lib/shared/widgets/add_new_server.dart
Normal file
107
lib/shared/widgets/add_new_server.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:whispering_pages/api/api_provider.dart';
|
||||
|
||||
class AddNewServer extends HookConsumerWidget {
|
||||
const AddNewServer({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.onPressed,
|
||||
this.readOnly = false,
|
||||
this.allowEmpty = false,
|
||||
});
|
||||
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// the function to call when the button is pressed
|
||||
final void Function()? onPressed;
|
||||
|
||||
/// if this field is read only
|
||||
final bool readOnly;
|
||||
|
||||
/// the server URI can be empty
|
||||
final bool allowEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final myController = controller ?? useTextEditingController();
|
||||
var newServerURI = useValueListenable(myController);
|
||||
final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text));
|
||||
bool isServerAliveValue = isServerAlive.when(
|
||||
data: (value) => value,
|
||||
loading: () => false,
|
||||
error: (error, _) => false,
|
||||
);
|
||||
|
||||
return TextFormField(
|
||||
readOnly: readOnly,
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Server URI',
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixText: 'https://',
|
||||
prefixIcon: Tooltip(
|
||||
message: newServerURI.text.isEmpty
|
||||
? 'Server Status'
|
||||
: isServerAliveValue
|
||||
? 'Server connected'
|
||||
: 'Cannot connect to server',
|
||||
child: newServerURI.text.isEmpty
|
||||
? Icon(
|
||||
Icons.cloud_outlined,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
)
|
||||
: isServerAlive.when(
|
||||
data: (value) {
|
||||
return value
|
||||
? Icon(
|
||||
Icons.cloud_done_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: Icon(
|
||||
Icons.cloud_off_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
},
|
||||
loading: () => Transform.scale(
|
||||
scale: 0.5,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, _) => Icon(
|
||||
Icons.cloud_off_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// add server button
|
||||
suffixIcon: onPressed == null
|
||||
? null
|
||||
: Container(
|
||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: IconButton.filled(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Add new server',
|
||||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
focusColor: Theme.of(context).colorScheme.onBackground,
|
||||
|
||||
// should be enabled when
|
||||
onPressed: !readOnly &&
|
||||
(isServerAliveValue ||
|
||||
(allowEmpty && newServerURI.text.isEmpty))
|
||||
? onPressed
|
||||
: null, // disable button if server is not alive
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// add to add to existing servers
|
||||
}
|
||||
}
|
||||
46
lib/shared/widgets/drawer.dart
Normal file
46
lib/shared/widgets/drawer.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:whispering_pages/pages/server_manager.dart';
|
||||
import 'package:whispering_pages/router/router.dart';
|
||||
|
||||
|
||||
class MyDrawer extends StatelessWidget {
|
||||
const MyDrawer({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
child: Text(
|
||||
'Whispering Pages',
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('server Settings'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ServerManagerPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('App Settings'),
|
||||
onTap: () {
|
||||
context.goNamed(Routes.settings.name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
124
lib/shared/widgets/expandable_description.dart
Normal file
124
lib/shared/widgets/expandable_description.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
class ExpandableDescription extends HookWidget {
|
||||
const ExpandableDescription({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.readMoreText = 'Read More',
|
||||
this.readLessText = 'Read Less',
|
||||
});
|
||||
|
||||
/// the title of the description section
|
||||
final String title;
|
||||
|
||||
/// the collapsible content
|
||||
final String content;
|
||||
|
||||
/// the text to show when the description is collapsed
|
||||
final String readMoreText;
|
||||
|
||||
/// the text to show when the description is expanded
|
||||
final String readLessText;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var isDescExpanded = useState(false);
|
||||
const duration = Duration(milliseconds: 300);
|
||||
final descriptionAnimationController = useAnimationController(
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
final themeData = Theme.of(context);
|
||||
final textTheme = themeData.textTheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// header with carrot icon is tapable
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
onTap: () {
|
||||
isDescExpanded.value = !isDescExpanded.value;
|
||||
if (isDescExpanded.value) {
|
||||
descriptionAnimationController.forward();
|
||||
} else {
|
||||
descriptionAnimationController.reverse();
|
||||
}
|
||||
},
|
||||
// a header with a carrot icon
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// header text
|
||||
Text(
|
||||
style: textTheme.titleMedium,
|
||||
title,
|
||||
),
|
||||
// carrot icon
|
||||
AnimatedRotation(
|
||||
turns: isDescExpanded.value ? 0.5 : 0,
|
||||
duration: duration,
|
||||
curve: Curves.easeInOutCubic,
|
||||
child: const Icon(Icons.expand_more_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// description with maxLines of 3
|
||||
// for now leave animation, just toggle the maxLines
|
||||
// TODO: add animation using custom ticker that will animate the maxLines
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: duration * 3,
|
||||
child: isDescExpanded.value
|
||||
? Text(
|
||||
style: textTheme.bodyMedium,
|
||||
content,
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
style: textTheme.bodyMedium,
|
||||
content,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// a Read More / Read Less button at the end of the description
|
||||
// if the description is expanded, then the button will say Read Less
|
||||
// if the description is collapsed, then the button will say Read More
|
||||
// the button will be at the end of the description
|
||||
// the button will be tapable
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
isDescExpanded.value = !isDescExpanded.value;
|
||||
if (isDescExpanded.value) {
|
||||
descriptionAnimationController.forward();
|
||||
} else {
|
||||
descriptionAnimationController.reverse();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: themeData.colorScheme.primary,
|
||||
),
|
||||
isDescExpanded.value ? readLessText : readMoreText,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/shared/widgets/shelves/author_shelf.dart
Normal file
82
lib/shared/widgets/shelves/author_shelf.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/api/image_provider.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/home_shelf.dart';
|
||||
|
||||
/// A shelf that displays Authors on the home page
|
||||
class AuthorHomeShelf extends HookConsumerWidget {
|
||||
const AuthorHomeShelf({
|
||||
super.key,
|
||||
required this.shelf,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final AuthorShelf shelf;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SimpleHomeShelf(
|
||||
title: title,
|
||||
children: shelf.entities
|
||||
.map(
|
||||
(item) => AuthorOnShelf(item: item),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a widget to display a item on the shelf
|
||||
class AuthorOnShelf extends HookConsumerWidget {
|
||||
const AuthorOnShelf({
|
||||
super.key,
|
||||
required this.item,
|
||||
});
|
||||
|
||||
final Author item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final author = AuthorMinified.fromJson(item.toJson());
|
||||
// final coverImage = ref.watch(coverImageProvider(item));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
||||
constraints: const BoxConstraints(maxWidth: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 50),
|
||||
// child: coverImage.when(
|
||||
// data: (image) {
|
||||
// return Image.memory(image, fit: BoxFit.cover);
|
||||
// },
|
||||
// loading: () {
|
||||
// return const Center(child: CircularProgressIndicator());
|
||||
// },
|
||||
// error: (error, stack) {
|
||||
// return const Icon(Icons.error);
|
||||
// },
|
||||
// ),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
child: Text(
|
||||
author.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
190
lib/shared/widgets/shelves/book_shelf.dart
Normal file
190
lib/shared/widgets/shelves/book_shelf.dart
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:shimmer/shimmer.dart' show Shimmer;
|
||||
import 'package:whispering_pages/api/image_provider.dart';
|
||||
import 'package:whispering_pages/constants/hero_tag_conventions.dart';
|
||||
import 'package:whispering_pages/router/models/library_item_extras.dart';
|
||||
import 'package:whispering_pages/router/router.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/home_shelf.dart';
|
||||
|
||||
/// A shelf that displays books on the home page
|
||||
class BookHomeShelf extends HookConsumerWidget {
|
||||
const BookHomeShelf({
|
||||
super.key,
|
||||
required this.shelf,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final LibraryItemShelf shelf;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SimpleHomeShelf(
|
||||
title: title,
|
||||
children: shelf.entities
|
||||
.map(
|
||||
(item) => switch (item.mediaType) {
|
||||
MediaType.book => BookOnShelf(
|
||||
item: item,
|
||||
key: ValueKey(shelf.id + item.id),
|
||||
heroTagSuffix: shelf.id,
|
||||
),
|
||||
_ => Container(),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a widget to display a item on the shelf
|
||||
class BookOnShelf extends HookConsumerWidget {
|
||||
const BookOnShelf({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.heroTagSuffix = '',
|
||||
});
|
||||
|
||||
final LibraryItem item;
|
||||
|
||||
/// makes the hero tag unique
|
||||
final String heroTagSuffix;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final book = BookMinified.fromJson(item.media.toJson());
|
||||
final metadata = BookMetadataMinified.fromJson(book.metadata.toJson());
|
||||
final coverImage = ref.watch(coverImageProvider(item));
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final height = min(constraints.maxHeight, 500);
|
||||
final width = height * 0.75;
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// the cover image of the book
|
||||
// take up remaining space hence the expanded
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Hero(
|
||||
tag: HeroTagPrefixes.bookCover + item.id + heroTagSuffix,
|
||||
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// open the book
|
||||
context.pushNamed(
|
||||
Routes.libraryItem.name,
|
||||
pathParameters: {
|
||||
Routes.libraryItem.pathParamName!: item.id,
|
||||
},
|
||||
extra: LibraryItemExtras(
|
||||
book: book,
|
||||
heroTagSuffix: heroTagSuffix,
|
||||
coverImage: coverImage.valueOrNull,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: coverImage.when(
|
||||
data: (image) {
|
||||
// return const BookCoverSkeleton();
|
||||
if (image.isEmpty) {
|
||||
return const Icon(Icons.error);
|
||||
}
|
||||
var imageWidget = Image.memory(
|
||||
image,
|
||||
fit: BoxFit.fill,
|
||||
cacheWidth: (height *
|
||||
1.2 *
|
||||
MediaQuery.of(context).devicePixelRatio)
|
||||
.round(),
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
child: imageWidget,
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return const Center(child: BookCoverSkeleton());
|
||||
},
|
||||
error: (error, stack) {
|
||||
return const Icon(Icons.error);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// the title and author of the book
|
||||
// AutoScrollText(
|
||||
Hero(
|
||||
tag: HeroTagPrefixes.bookTitle + item.id + heroTagSuffix,
|
||||
child: Text(
|
||||
metadata.title ?? '',
|
||||
// mode: AutoScrollTextMode.bouncing,
|
||||
// curve: Curves.easeInOut,
|
||||
// velocity: const Velocity(pixelsPerSecond: Offset(15, 0)),
|
||||
// delayBefore: const Duration(seconds: 2),
|
||||
// pauseBetween: const Duration(seconds: 2),
|
||||
// numberOfReps: 15,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Hero(
|
||||
tag: HeroTagPrefixes.authorName + item.id + heroTagSuffix,
|
||||
child: Text(
|
||||
metadata.authorName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a skeleton for the book cover
|
||||
class BookCoverSkeleton extends StatelessWidget {
|
||||
const BookCoverSkeleton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: Theme.of(context).colorScheme.surface.withOpacity(0.3),
|
||||
highlightColor:
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.1),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/shared/widgets/shelves/home_shelf.dart
Normal file
117
lib/shared/widgets/shelves/home_shelf.dart
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/author_shelf.dart';
|
||||
import 'package:whispering_pages/shared/widgets/shelves/book_shelf.dart';
|
||||
|
||||
/// A shelf that displays books/authors/series on the home page
|
||||
///
|
||||
/// this will build the appropriate shelf based on the type of the shelf
|
||||
class HomeShelf extends HookConsumerWidget {
|
||||
const HomeShelf({
|
||||
super.key,
|
||||
required this.shelf,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final Shelf shelf;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return switch (shelf.type) {
|
||||
ShelfType.book => BookHomeShelf(
|
||||
title: title,
|
||||
shelf: LibraryItemShelf.fromJson(shelf.toJson()),
|
||||
),
|
||||
ShelfType.authors => AuthorHomeShelf(
|
||||
title: title,
|
||||
shelf: AuthorShelf.fromJson(shelf.toJson()),
|
||||
),
|
||||
_ => Container(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A shelf that displays children on the home page
|
||||
class SimpleHomeShelf extends HookConsumerWidget {
|
||||
const SimpleHomeShelf({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.title,
|
||||
this.height,
|
||||
});
|
||||
|
||||
/// the title of the shelf
|
||||
final String title;
|
||||
|
||||
/// the children to display on the shelf
|
||||
final List<Widget> children;
|
||||
final double? height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// if height is null take up 30% of the smallest screen dimension
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, bottom: 8.0),
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
),
|
||||
// fix the height of the shelf as a percentage of the screen height
|
||||
SizedBox(
|
||||
height: height ?? getDefaultShelfHeight(context, perCent: 0.5),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 || index == children.length + 1) {
|
||||
return const SizedBox(
|
||||
width: 8,
|
||||
);
|
||||
}
|
||||
return children[index - 1];
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
if (index == 0 || index == children.length + 1) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return const SizedBox(width: 16);
|
||||
},
|
||||
itemCount: children.length +
|
||||
2, // add some extra space at the start and end so that the first and last items are not at the edge
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// get the height of the shelf based on the screen size
|
||||
/// the height is the height parent wants the shelf to be
|
||||
/// but it should not be less than 150 so we take the max of 150 and the height in the end
|
||||
/// ignoreWidth is used to ignore the width of the screen and take only the height into consideration else smallest side is taken so that shelf is not too big on tablets
|
||||
double getDefaultShelfHeight(
|
||||
BuildContext context, {
|
||||
bool ignoreWidth = false,
|
||||
atMin = 150.0,
|
||||
perCent = 0.3,
|
||||
}) {
|
||||
double referenceSide;
|
||||
if (ignoreWidth) {
|
||||
referenceSide = MediaQuery.of(context).size.height;
|
||||
} else {
|
||||
referenceSide = min(
|
||||
MediaQuery.of(context).size.width,
|
||||
MediaQuery.of(context).size.height,
|
||||
);
|
||||
}
|
||||
return max(atMin, referenceSide * perCent);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue