something

This commit is contained in:
Dr-Blank 2024-05-08 05:03:49 -04:00
parent dbf4ce1959
commit a720c977c2
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
115 changed files with 8819 additions and 1 deletions

View 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
}
}

49
lib/widgets/drawer.dart Normal file
View file

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:whispering_pages/pages/app_settings.dart';
import 'package:whispering_pages/pages/server_manager.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: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AppSettingsPage(),
),
);
},
),
],
),
);
}
}

View 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/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 Widget 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,
),
),
],
),
);
}
}

View file

@ -0,0 +1,118 @@
import 'package:auto_scroll_text/auto_scroll_text.dart';
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/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 Widget 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),
),
_ => Container(),
},
)
.toList(),
);
}
}
// a widget to display a item on the shelf
class BookOnShelf extends HookConsumerWidget {
const BookOnShelf({
super.key,
required this.item,
});
final LibraryItem item;
@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));
const coverSize = 150.0;
return Container(
margin: const EdgeInsets.only(right: 10, bottom: 10),
constraints: const BoxConstraints(maxWidth: coverSize),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: AspectRatio(
aspectRatio: 1,
child: Container(
constraints: const BoxConstraints(maxWidth: coverSize),
color: Colors.grey[800],
child: coverImage.when(
data: (image) {
if (image.isEmpty) {
return const Icon(Icons.error);
}
return Image.memory(
image,
fit: BoxFit.cover,
cacheWidth:
(coverSize * MediaQuery.of(context).devicePixelRatio)
.round(),
);
},
loading: () {
return const Center(child: CircularProgressIndicator());
},
error: (error, stack) {
return const Icon(Icons.error);
},
),
),
),
),
Container(
margin: const EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AutoScrollText(
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,
),
const SizedBox(height: 3),
Text(
metadata.authorName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/widgets/shelves/author_shelf.dart';
import 'package:whispering_pages/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 Widget 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 books on the home page
class SimpleHomeShelf extends HookConsumerWidget {
const SimpleHomeShelf({
super.key,
required this.children,
required this.title,
});
final Widget title;
final List<Widget> children;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: children,
),
),
],
),
);
}
}

119
lib/widgets/user_login.dart Normal file
View file

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lottie/lottie.dart';
import 'package:whispering_pages/hacks/fix_autofill_losing_focus.dart';
class UserLogin extends HookConsumerWidget {
UserLogin({
super.key,
this.usernameController,
this.passwordController,
this.onPressed,
});
TextEditingController? usernameController;
TextEditingController? passwordController;
final void Function()? onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
usernameController ??= useTextEditingController();
passwordController ??= useTextEditingController();
final isPasswordVisibleAnimationController = useAnimationController(
duration: const Duration(milliseconds: 500),
);
var isPasswordVisible = useState(false);
// forward animation when the password visibility changes
useEffect(
() {
if (isPasswordVisible.value) {
isPasswordVisibleAnimationController.forward();
} else {
isPasswordVisibleAnimationController.reverse();
}
return null;
},
[isPasswordVisible.value],
);
return Center(
child: InactiveFocusScopeObserver(
child: AutofillGroup(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: usernameController,
autofocus: true,
autofillHints: const [AutofillHints.username],
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: 'Username',
labelStyle: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.8),
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: passwordController,
autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.done,
obscureText: !isPasswordVisible.value,
onFieldSubmitted: onPressed != null
? (_) {
onPressed!();
}
: null,
decoration: InputDecoration(
labelText: 'Password',
labelStyle: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.8),
),
border: const OutlineInputBorder(),
suffixIcon: ColorFiltered(
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary.withOpacity(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,
),
),
),
),
suffixIconConstraints: const BoxConstraints(
maxHeight: 45,
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: onPressed,
child: const Text('Login'),
),
],
),
),
),
);
}
}