(ui): Removed all deprecated UI elements, added scroll-back support to entire UI, clean-up codebase (#396)

* Added RadarrManualImportConfigure for manual import config options

* Default bottom modal sheet to not expand

* Shrinkwrap modal listviews around content

* use keyboardDismissBehavior for all ListView

* Ensure keyboard is unfocused when opening the drawer

* Fetch and store quality definitions in global Radarr state

* Ability to configure language and quality profile for manual import instances

* Reworking all search bar implementations, integrated scroll back for Lidarr across module

* (ui): [Dashboard] Clean up of dashboard code, integrated scroll to top support

* (ui): [SnackBar] Remove all deprecated uses of LSSnackBar, add scrollback for NZBGet, improve error handling in queue

* (ui): Removed usage of old StickyListView, ReorderableListView, integrated LunaScaffold into base routes

* (flutter): Update packages

* (flutter): Remove flutter_sticky_header package

* (ui): Remove unused deprecated classes

* (chore): Update changelog

* (fix): [Dashboard/Calendar] Radarr releases were not considering digital release date

* (ui): [Sonarr] Adapted for scroll back to top functionality, remove more deprecated UI elements

* (ui): Refactored Tautulli, Removed ALL deprecated UI elements from LunaSea

* (ui): Fixed inconsistent usage of material and luna colours across UI

* (fix): [Sonarr/Radarr] Do not show "Search For..." button when the query is empty, but there are no results

* (chore): Updated changelog

* (fix): [Sonarr/Add] Fix search queries not gettting executed

* (fix): [Tautulli] Remove unused global state elements

* (feat): [UI/AppBar] Ability to hide the leading widget

* (fix): [Radarr/Languages] Fix "Unknown" language not being shown in selection dialog

* (feat): [Radarr/Import] Ability to search movies to select, toggle real and proper toggles

* (chore): Updated packages
This commit is contained in:
Jagandeep Brar
2021-04-01 22:06:07 -05:00
committed by GitHub
parent 45b0227b08
commit 139c9a0cd3
436 changed files with 7712 additions and 9441 deletions

View File

@@ -3,25 +3,42 @@
## GitHub
#### NEW
- `[Everything]` All lists now have scroll-back support by tapping the AppBar
- `[Radarr/Catalogue]` Ability to sort by in cinema, physical release, and digital release dates
- `[Radarr/Details]` Show the monitor state of the movie in the overview table
- `[Radarr/Manual Import]` Ability to manually import content from the filesystem (quick)
- `[Sonarr/Add]` View a series' TheTVDB page by long-pressing the tile
- `[Radarr/Manual Import]` Ability to manually import content from the filesystem
- `[Sonarr/Add]` View a series' TheTVDB page by long-pressing the tile in the search list
- `[Tautulli/Webhooks]` Support for deep-linking to content through notifications
#### TWEAKS
- `[Dashboard]` Removed month header from calendar
- `[Dashboard]` Disable calendar days outside of the past and future date range
- `[Lidarr/Artist]` Overview is now displayed as a table, to match Radarr and Sonarr
- `[NZBGet/Status]` Status and statistics are now shown in a table
- `[SABnzbd/Status]` Status and statistics are now shown in a table
- `[UI/UX]` Minor tweaks to the colour palette
- `[UI/UX]` Normalized the design and size of all action buttons
- `[UI/UX]` Sticky primary button actions to the bottom navigation bar area
- `[UI/UX]` Unify the design and size of all action buttons
- `[UI/UX]` Sticky primary action buttons to the bottom navigation bar area
- `[UI/UX]` Unified the design of all expandable cards in their expanded state
- `[UI/UX]` Bottom modal sheets lists will now shrink wrap to match content height
- `[UI/UX]` All ListViews will now dismiss the keyboard on scroll gestures
- `[UI/UX]` Opening the drawer will now ensure that the keyboard gets dismissed
- `[UI/UX]` Removed all deprecated UI elements and usages
#### FIXES
- `[Backup & Restore]` Indexers would not get restored with backups
- `[Dashboard]` Radarr releases were not considering the digital release date
- `[Dialogs]` Checkbox tiles were padded too aggressively in the X-axis
- `[Errors]` All error snackbars will now have a button to view the error
- `[Networking]` Headers would not get attached in some networking configurations
- `[NZBGet/Queue]` Queue would not refresh in some cases when switching profiles
- `[Radarr/Catalogue]` "Search For..." would be shown when there were no results and the query was empty
- `[Radarr/Details]` The overview table would not update after editing a movie
- `[SABnzbd]` Queue would not refresh in some cases when switching profiles
- `[Sonarr/Add]` Series type would not be set correctly when adding a new series
- `[Tautulli/Activity]` Activity header included some incorrectly localized strings
- `[UI/UX]` Remove all uses of the deprecated snackbar
- `[UI/UX]` Some elements were using the system colours instead of the LunaSea colours
- `[Webhooks]` Push the module's home route when receiving malformed data blocks from webhooks
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -6,7 +6,9 @@
"lunasea.ComingSoon": "Coming Soon",
"lunasea.Dashboard": "Dashboard",
"lunasea.GoBack": "Go Back",
"lunasea.GoToSettings": "Go to Settings",
"lunasea.ModuleIsNotEnabled": "{} Is Not Enabled",
"lunasea.NoModulesEnabled": "No Modules Enabled",
"lunasea.Refresh": "Refresh",
"lunasea.ReturnToDashboard": "Return to Dashboard",
"lunasea.SearchTextBar": "Search…",
@@ -45,7 +47,7 @@
"radarr.InCinemasIn": "In Cinemas in {}",
"radarr.InCinemasToday": "In Cinemas Today",
"radarr.Interactive": "Interactive",
"radarr.Language": "Language",
"radarr.Languages": "Languages",
"radarr.ManualImport": "Manual Import",
"radarr.ManualImportDescription": "Import Movies from the Filesystem",
"radarr.MinimumAvailability": "Minimum Availability",
@@ -87,6 +89,9 @@
"radarr.SearchAllMissing": "Search All Missing",
"radarr.SearchFor": "Search for {}",
"radarr.Seeders": "Seeders",
"radarr.SelectLanguage": "Select Languages",
"radarr.SelectMovie": "Select Movie",
"radarr.SelectQuality": "Select Quality",
"radarr.Size": "Size",
"radarr.SortCatalogue": "Sort Catalogue",
"radarr.Studio": "Studio",

View File

@@ -1,20 +1,6 @@
class Constants {
Constants._();
static const TEXT_EMDASH = '';
static const TEXT_BULLET = '';
static const TEXT_ELLIPSIS = '';
static const UI_BORDER_RADIUS = 10.0;
static const UI_FONT_SIZE_HEADER = 18.0;
static const UI_FONT_SIZE_STICKYHEADER = 14.0;
static const UI_FONT_SIZE_SUBHEADER = 12.0;
static const UI_FONT_SIZE_TITLE = 16.0;
static const UI_FONT_SIZE_SUBTITLE = 13.0;
static const UI_FONT_SIZE_GRAPH_LEGEND = 10.0;
static const NO_SERVICES_ENABLED = '<<NO_SERVICES_ENABLED>>';
static const URL_CHANGELOG = 'https://github.com/CometTools/LunaSea/blob/master/CHANGELOG.md';
static const URL_CRASHLYTICS = 'https://firebase.google.com/products/crashlytics';
static const URL_DISCORD = 'https://www.lunasea.app/discord';
@@ -26,10 +12,4 @@ class Constants {
static const URL_WEBLATE = 'https:/www.lunasea.app/translate';
static const URL_WEBSITE = 'https://www.lunasea.app';
static const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15';
static const Map historyReasonMessages = {
'Upgrade': 'Upgraded File',
'MissingFromDisk': 'Missing From Disk',
'Manual': 'Manually Removed',
};
}

View File

@@ -22,20 +22,20 @@ class LunaDialogs {
}
}
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: dialogTitle,
buttons: [
LSDialog.button(
LunaDialog.button(
text: 'Save',
onPressed: () => _setValues(true),
),
],
content: [
if((extraText?.length ?? 0) != 0) LSDialog.richText(children: extraText),
if((extraText?.length ?? 0) != 0) LunaDialog.richText(children: extraText),
Form(
key: _formKey,
child: LSDialog.textFormInput(
child: LunaDialog.textFormInput(
controller: _textController,
title: dialogTitle,
onSubmitted: (_) => _setValues(true),
@@ -43,7 +43,7 @@ class LunaDialogs {
),
),
],
contentPadding: (extraText?.length ?? 0) == 0 ? LSDialog.inputDialogContentPadding() : LSDialog.inputTextDialogContentPadding(),
contentPadding: (extraText?.length ?? 0) == 0 ? LunaDialog.inputDialogContentPadding() : LunaDialog.inputTextDialogContentPadding(),
);
return Tuple2(_flag, _textController.text);
}
@@ -52,12 +52,12 @@ class LunaDialogs {
///
/// Can pass in boolean [alignLeft] to left align the text in the dialog (useful for bulleted lists)
Future<void> textPreview(BuildContext context, String dialogTitle, String text, { bool alignLeft = false }) async {
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: dialogTitle,
cancelButtonText: 'Close',
buttons: [
LSDialog.button(
LunaDialog.button(
text: 'Copy',
onPressed: () async {
await Clipboard.setData(ClipboardData(text: text));
@@ -67,27 +67,27 @@ class LunaDialogs {
),
],
content: [
LSDialog.textContent(text: text),
LunaDialog.textContent(text: text),
],
contentPadding: LSDialog.textDialogContentPadding(),
contentPadding: LunaDialog.textDialogContentPadding(),
);
}
Future<void> showRejections(BuildContext context, List<String> rejections) async {
if((rejections ?? []).isEmpty) return textPreview(context, 'Rejection Reasons', 'No rejections found');
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: 'Rejection Reasons',
cancelButtonText: 'Close',
content: List.generate(
rejections.length,
(index) => LSDialog.tile(
(index) => LunaDialog.tile(
text: rejections[index],
icon: Icons.report_outlined,
iconColor: LunaColours.red,
),
),
contentPadding: LSDialog.listDialogContentPadding(),
contentPadding: LunaDialog.listDialogContentPadding(),
);
}
@@ -102,20 +102,20 @@ class LunaDialogs {
Navigator.of(context, rootNavigator: true).pop();
}
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: 'Delete All Files',
buttons: [
LSDialog.button(
LunaDialog.button(
text: 'Delete',
textColor: LunaColours.red,
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $moduleTitle?'),
LunaDialog.textContent(text: 'Are you sure you want to delete all the files and folders for $moduleTitle?'),
],
contentPadding: LSDialog.textDialogContentPadding(),
contentPadding: LunaDialog.textDialogContentPadding(),
);
return [_flag];
}

View File

@@ -5,7 +5,7 @@ extension DoubleExtension on double {
///
/// Time greater than 48 hours => "x Days Ago" else "x Hours Ago"
String lunaHoursToAge() {
if(this == null) return Constants.TEXT_EMDASH;
if(this == null) return LunaUI.TEXT_EMDASH;
int days = (this/24).floor();
if(this > 48) return '$days Days Ago';
return this == 1 ? '1 Hour Ago' : '${this.toStringAsFixed(1)} Hours Ago';

View File

@@ -25,7 +25,7 @@ extension DurationExtension on Duration {
if(days != null) days,
if(hours != null) hours,
if(minutes != null) minutes,
if(minutes == null) Constants.TEXT_EMDASH,
if(minutes == null) LunaUI.TEXT_EMDASH,
].join(' ');
}
}

View File

@@ -83,7 +83,7 @@ extension LunaModuleExtension on LunaModule {
String get route {
switch(this) {
case LunaModule.DASHBOARD: return Dashboard.ROUTE_NAME;
case LunaModule.DASHBOARD: return DashboardHomeRouter().route();
case LunaModule.LIDARR: return Lidarr.ROUTE_NAME;
case LunaModule.NZBGET: return NZBGet.ROUTE_NAME;
case LunaModule.RADARR: return RadarrHomeRouter().route();

View File

@@ -10,6 +10,7 @@ class LunaRouter {
/// Calls `defineAllRoutes()` on all module routers that implement [LunaModuleRouter].
void intialize() {
router.notFoundHandler = Handler(handlerFunc: (context, params) => LunaInvalidRoute());
DashboardRouter().defineAllRoutes(router);
RadarrRouter().defineAllRoutes(router);
SearchRouter().defineAllRoutes(router);
SettingsRouter().defineAllRoutes(router);
@@ -21,7 +22,6 @@ class LunaRouter {
///
/// Returns a map of all module routes.
Map<String, WidgetBuilder> get routes => <String, WidgetBuilder> {
Dashboard.ROUTE_NAME: (context) => Dashboard(),
..._lidarr,
..._sabnzbd,
..._nzbget,

View File

@@ -9,9 +9,10 @@ export 'ui/colors.dart';
export 'ui/containers.dart';
export 'ui/controllers.dart';
export 'ui/decoration.dart';
export 'ui/deprecated.dart';
export 'ui/dialog.dart';
export 'ui/divider.dart';
export 'ui/drawer.dart';
export 'ui/floating_action_button.dart';
export 'ui/header.dart';
export 'ui/highlighted_node.dart';
export 'ui/icons.dart';
@@ -23,13 +24,14 @@ export 'ui/message.dart';
export 'ui/network_image.dart';
export 'ui/popup_menu_button.dart';
export 'ui/refresh_indicator.dart';
export 'ui/scaffold.dart';
export 'ui/shape.dart';
export 'ui/sliver_sticky_header.dart';
export 'ui/snackbar.dart';
export 'ui/switch.dart';
export 'ui/table.dart';
export 'ui/text.dart';
export 'ui/theme.dart';
export 'ui/will_pop_scope.dart';
class LunaUI {
// Text Constants

View File

@@ -13,6 +13,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
final String title;
final List<Widget> actions;
final PreferredSizeWidget bottom;
final bool hideLeading;
final bool useDrawer;
final Widget child;
final double height;
@@ -22,7 +23,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
@override
Size get preferredSize {
double _size = kToolbarHeight+6.0;
double _size = (height ?? kToolbarHeight);
if(bottom != null) _size += bottom.preferredSize.height;
return new Size.fromHeight(_size);
}
@@ -38,6 +39,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
this.profiles,
this.pageController,
this.scrollControllers,
this.hideLeading = false,
});
/// Create a new [AppBar] widget pre-styled for LunaSea.
@@ -52,6 +54,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
List<Widget> actions,
PreferredSizeWidget bottom,
bool useDrawer = false,
bool hideLeading = false,
PageController pageController,
List<ScrollController> scrollControllers,
}) {
@@ -64,6 +67,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
useDrawer: useDrawer,
pageController: pageController,
scrollControllers: scrollControllers,
hideLeading: hideLeading,
type: _APPBAR_TYPE.DEFAULT,
);
}
@@ -71,14 +75,23 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
/// Create a new, empty [LunaAppBar] which can be used to attach to a [Scaffold] in a [PageView] that is already wrapped in an [AppBar].
///
/// Example usages would be a [PageView] but a single page needs an [AppBar] bottom widget.
///
/// The default padding is for a [LunaTextInputBar].
factory LunaAppBar.empty({
@required Widget child,
@required double height,
EdgeInsets padding = LunaTextInputBar.appBarMargin,
Alignment alignment = Alignment.topCenter,
}) {
assert(child != null);
assert(height != null);
return LunaAppBar._internal(
child: child,
child: Container(
child: child,
height: height,
padding: padding,
alignment: alignment,
),
height: height,
useDrawer: false,
type: _APPBAR_TYPE.EMPTY,
@@ -96,6 +109,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
@required String title,
@required List<String> profiles,
bool useDrawer = true,
bool hideLeading = false,
List<Widget> actions,
PageController pageController,
List<ScrollController> scrollControllers,
@@ -107,6 +121,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
title: title,
actions: actions,
useDrawer: useDrawer,
hideLeading: hideLeading,
pageController: pageController,
scrollControllers: scrollControllers,
type: _APPBAR_TYPE.DEFAULT,
@@ -116,6 +131,7 @@ class LunaAppBar extends StatefulWidget implements PreferredSizeWidget {
profiles: profiles,
actions: actions,
useDrawer: useDrawer,
hideLeading: hideLeading,
pageController: pageController,
scrollControllers: scrollControllers,
type: _APPBAR_TYPE.DROPDOWN,
@@ -176,11 +192,15 @@ class _State extends State<LunaAppBar> {
}
Widget _sharedLeading(BuildContext context) {
if(widget.hideLeading ?? false) return null;
if(widget.useDrawer) return IconButton(
icon: Icon(Icons.menu_rounded),
onPressed: () async {
HapticFeedback.lightImpact();
if(Scaffold.of(context).hasDrawer) Scaffold.of(context).openDrawer();
if(Scaffold.of(context).hasDrawer) {
Scaffold.of(context).openDrawer();
FocusManager.instance.primaryFocus?.unfocus();
}
},
);
return InkWell(
@@ -205,6 +225,7 @@ class _State extends State<LunaAppBar> {
style: TextStyle(fontSize: LunaUI.FONT_SIZE_APP_BAR),
),
leading: _sharedLeading(context),
automaticallyImplyLeading: !(widget.hideLeading ?? false),
centerTitle: false,
elevation: 0,
actions: widget.actions,
@@ -233,7 +254,7 @@ class _State extends State<LunaAppBar> {
children: [
Text(
widget.title,
style: TextStyle(fontSize: Constants.UI_FONT_SIZE_HEADER),
style: TextStyle(fontSize: LunaUI.FONT_SIZE_HEADER),
),
Icon(Icons.arrow_drop_down),
],
@@ -248,7 +269,7 @@ class _State extends State<LunaAppBar> {
child: Text(
profile,
style: TextStyle(
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
)];

View File

@@ -36,10 +36,7 @@ class LunaBottomActionBar extends StatelessWidget {
padding: padding,
),
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
);
}
}

View File

@@ -57,7 +57,7 @@ class _State extends State<LunaBottomNavigationBar> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if(widget.topActions != null && widget.topActions.length != 0) LunaBottomActionBar(
if((widget.topActions?.length ?? 0) != 0) LunaBottomActionBar(
actions: widget.topActions,
useSafeArea: false,
),

View File

@@ -7,7 +7,7 @@ class LunaBottomModalSheet {
Future<dynamic> showModal({
@required BuildContext context,
@required Widget Function(BuildContext) builder,
bool expand = true,
bool expand = false,
}) async => showBarModalBottomSheet(
context: context,
expand: expand,

View File

@@ -1,16 +0,0 @@
export 'deprecated/card.dart';
export 'deprecated/containers.dart';
export 'deprecated/description_block.dart';
export 'deprecated/dialogs.dart';
export 'deprecated/floating_action_button.dart';
export 'deprecated/icon.dart';
export 'deprecated/input_bar.dart';
export 'deprecated/listview.dart';
export 'deprecated/loader.dart';
export 'deprecated/messages.dart';
export 'deprecated/navigation_bar.dart';
export 'deprecated/network_image.dart';
export 'deprecated/refresh_indicator.dart';
export 'deprecated/snackbar.dart';
export 'deprecated/table.dart';
export 'deprecated/text.dart';

View File

@@ -1,2 +0,0 @@
export 'card/card.dart';
export 'card/card_tile.dart';

View File

@@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSCard extends StatelessWidget {
final Widget child;
final EdgeInsets margin;
final bool reducedMargin;
final Color color;
final Decoration decoration;
final double height;
LSCard({
@required this.child,
this.margin = LunaUI.MARGIN_CARD,
this.reducedMargin = false,
this.color,
this.decoration,
this.height,
});
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [
LunaDatabaseValue.THEME_AMOLED.key,
LunaDatabaseValue.THEME_AMOLED_BORDER.key,
]),
builder: (context, box, widget) => Card(
child: Container(
child: child,
decoration: decoration,
height: height,
),
elevation: LunaUI.ELEVATION,
margin: reducedMargin
? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0)
: margin,
color: color == null
? Theme.of(context).primaryColor
: color,
shape: LunaUI.shapeBorder,
),
);
}

View File

@@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSCardTile extends StatelessWidget {
final Widget title;
final Widget subtitle;
final Widget trailing;
final Widget leading;
final Function onTap;
final Function onLongPress;
final bool padContent;
final bool reducedMargin;
final EdgeInsets customPadding;
final EdgeInsets customMargin;
final Decoration decoration;
final Color color;
LSCardTile({
@required this.title,
this.subtitle,
this.trailing,
this.leading,
this.onTap,
this.onLongPress,
this.padContent = false,
this.decoration,
this.customMargin,
this.reducedMargin = false,
this.customPadding,
this.color,
});
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [
LunaDatabaseValue.THEME_AMOLED.key,
LunaDatabaseValue.THEME_AMOLED_BORDER.key,
]),
builder: (context, box, widget) => Card(
child: Container(
child: InkWell(
child: ListTile(
title: title,
subtitle: subtitle,
trailing: trailing,
leading: leading,
contentPadding: customPadding == null
? padContent
? EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0)
: null
: customPadding,
),
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
onTap: onTap,
onLongPress: onLongPress,
),
decoration: decoration,
),
margin: customMargin == null
? reducedMargin
? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0)
: LunaUI.MARGIN_CARD
: customMargin,
elevation: LunaUI.ELEVATION,
shape: LunaUI.shapeBorder,
color: color == null
? Theme.of(context).primaryColor
: color,
),
);
}

View File

@@ -1 +0,0 @@
export 'containers/row.dart';

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
class LSContainerRow extends StatelessWidget {
final List<Widget> children;
final EdgeInsets padding;
final Color backgroundColor;
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
LSContainerRow({
@required this.children,
this.backgroundColor,
this.padding = const EdgeInsets.symmetric(horizontal: 6.0),
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.start,
});
@override
Widget build(BuildContext context) => Padding(
child: Container(
child: Row(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
),
decoration: BoxDecoration(
color: backgroundColor,
),
),
padding: padding,
);
}

View File

@@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSFloatingActionButton extends StatelessWidget {
final Object heroTag;
final IconData icon;
final Color iconColor;
final Color backgroundColor;
final Function onPressed;
LSFloatingActionButton({
@required this.onPressed,
@required this.icon,
this.backgroundColor,
this.iconColor = Colors.white,
this.heroTag,
});
@override
Widget build(BuildContext context) => FloatingActionButton(
child: Icon(icon, color: iconColor),
heroTag: heroTag,
onPressed: onPressed,
backgroundColor: backgroundColor == null
? LunaColours.accent
: backgroundColor,
);
}

View File

@@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSFloatingActionButtonAnimated extends StatelessWidget {
final Object heroTag;
final AnimatedIconData icon;
final AnimationController controller;
final Color iconColor;
final Color backgroundColor;
final Function onPressed;
LSFloatingActionButtonAnimated({
@required this.onPressed,
@required this.icon,
@required this.controller,
this.backgroundColor,
this.iconColor = Colors.white,
this.heroTag,
});
@override
Widget build(BuildContext context) => FloatingActionButton(
child: AnimatedIcon(
icon: icon,
color: iconColor,
progress: controller,
),
heroTag: heroTag,
onPressed: onPressed,
backgroundColor: backgroundColor == null
? LunaColours.accent
: backgroundColor,
);
}

View File

@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSFloatingActionButtonExtended extends StatelessWidget {
final String label;
final Object heroTag;
final IconData icon;
final Color iconColor;
final Color labelColor;
final Color backgroundColor;
final Function onPressed;
LSFloatingActionButtonExtended({
@required this.label,
@required this.onPressed,
@required this.icon,
this.backgroundColor,
this.labelColor = Colors.white,
this.iconColor = Colors.white,
this.heroTag,
});
@override
Widget build(BuildContext context) => FloatingActionButton.extended(
icon: Icon(icon, color: iconColor),
label: Text(
label,
style: TextStyle(
color: labelColor,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
letterSpacing: 0.25,
),
),
heroTag: heroTag,
onPressed: onPressed,
backgroundColor: backgroundColor == null
? LunaColours.accent
: backgroundColor,
);
}

View File

@@ -1 +0,0 @@
export 'icon/icon_button.dart';

View File

@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
class LSIconButton extends StatelessWidget {
final IconData icon;
final String text;
final Color color;
final Function onPressed;
final Function onLongPress;
final double iconSize;
final double textSize;
LSIconButton({
this.icon,
this.text,
this.color = Colors.white,
this.onPressed,
this.onLongPress,
this.iconSize = 24.0,
this.textSize = 10.0,
});
@override
Widget build(BuildContext context) {
return InkWell(
child: IconButton(
icon: text != null
? Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
color: color,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: textSize,
),
)
: Icon(icon, color: color),
iconSize: iconSize,
onPressed: onPressed == null ? null : () async {
HapticFeedback.lightImpact();
onPressed();
}
),
onLongPress: onLongPress == null ? null : () async {
HapticFeedback.heavyImpact();
onLongPress();
},
);
}
}

View File

@@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSTextInputBar extends StatefulWidget {
final TextEditingController controller;
final TextInputAction action;
final TextInputType keyboardType;
final Iterable<String> autofillHints;
final String labelText;
final IconData labelIcon;
final bool autofocus;
final bool obscureText;
final bool isFormField;
final void Function(String, bool) onChanged;
final void Function(String) onSubmitted;
final String Function(String) validator;
final EdgeInsets margin;
LSTextInputBar({
@required this.controller,
this.onChanged,
this.onSubmitted,
this.validator,
this.autofillHints,
this.action = TextInputAction.search,
this.keyboardType = TextInputType.text,
this.labelText = 'Search...',
this.labelIcon = Icons.search,
this.margin = LunaUI.MARGIN_CARD,
this.autofocus = false,
this.obscureText = false,
this.isFormField = false,
});
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LSTextInputBar> {
bool _isFocused = false;
@override
Widget build(BuildContext context) => LSCard(
margin: widget.margin,
child: FocusScope(
child: Focus(
onFocusChange: (value) => setState(() => _isFocused = value),
child: widget.isFormField ? _isForm : _isNotForm,
),
),
color: Theme.of(context).canvasColor,
);
TextStyle get _sharedTextStyle => TextStyle(
color: Colors.white,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
);
InputDecoration get _sharedInputDecoration => InputDecoration(
labelText: widget.labelText,
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
suffixIcon: AnimatedOpacity(
child: GestureDetector(
child: Icon(
Icons.close,
color: LunaColours.accent,
size: 24.0,
),
onTap: () => widget?.onChanged('', true),
),
opacity: !_isFocused || widget.controller.text == '' ? 0.0 : 1.0,
duration: Duration(milliseconds: 200),
),
icon: Padding(
child: Icon(
widget.labelIcon,
color: LunaColours.accent,
),
padding: EdgeInsets.only(left: 16.0),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 8.0),
);
TextFormField get _isForm => TextFormField(
autofillHints: widget.autofillHints,
autofocus: widget.autofocus,
controller: widget.controller,
decoration: _sharedInputDecoration,
style: _sharedTextStyle,
cursorColor: LunaColours.accent,
textInputAction: widget.action,
obscureText: widget.obscureText,
autocorrect: false,
keyboardType: widget.keyboardType,
validator: widget?.validator,
onChanged: widget.onChanged == null ? null : (value) => widget.onChanged(value, false),
onFieldSubmitted: widget?.onSubmitted,
);
TextField get _isNotForm => TextField(
autofillHints: widget.autofillHints,
autofocus: widget.autofocus,
controller: widget.controller,
decoration: _sharedInputDecoration,
style: _sharedTextStyle,
cursorColor: LunaColours.accent,
textInputAction: widget.action,
obscureText: widget.obscureText,
autocorrect: false,
keyboardType: widget.keyboardType,
onChanged: widget.onChanged == null ? null : (value) => widget.onChanged(value, false),
onSubmitted: widget?.onSubmitted,
);
}

View File

@@ -1,4 +0,0 @@
export 'listview/listview.dart';
export 'listview/listview_builder.dart';
export 'listview/listview_reorderable.dart';
export 'listview/listview_stickyheader.dart';

View File

@@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
class LSListView extends StatelessWidget {
final List<Widget> children;
final EdgeInsetsGeometry customPadding;
final ScrollController controller;
LSListView({
@required this.children,
this.controller,
this.customPadding,
});
@override
Widget build(BuildContext context) => Scrollbar(
controller: controller == null ? ScrollController() : controller,
child: ListView(
controller: controller,
children: children,
padding: customPadding != null ? customPadding : EdgeInsets.only(
top: 8.0,
bottom: 8.0+(MediaQuery.of(context).padding.bottom),
),
physics: AlwaysScrollableScrollPhysics(),
),
);
}

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
class LSListViewBuilder extends StatelessWidget {
final int itemCount;
final bool reverse;
final Function(BuildContext context, int index) itemBuilder;
final EdgeInsetsGeometry customPadding;
final ScrollController controller = ScrollController();
LSListViewBuilder({
@required this.itemCount,
@required this.itemBuilder,
this.reverse = false,
this.customPadding,
});
@override
Widget build(BuildContext context) => Scrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
itemCount: itemCount,
itemBuilder: itemBuilder,
padding: customPadding != null ? customPadding : EdgeInsets.only(
top: 8.0,
bottom: 8.0+(MediaQuery.of(context).padding.bottom),
),
reverse: reverse,
physics: AlwaysScrollableScrollPhysics(),
),
);
}

View File

@@ -1,594 +0,0 @@
/*
* Modification of a Flutter-core file, adjusted Line 197
* static const double _dropAreaMargin = 8.0; //Used to be 100.0
*/
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
// Examples can assume:
// class MyDataObject { }
/// The callback used by [ReorderableListView] to move an item to a new
/// position in a list.
///
/// Implementations should remove the corresponding list item at [oldIndex]
/// and reinsert it at [newIndex].
///
/// If [oldIndex] is before [newIndex], removing the item at [oldIndex] from the
/// list will reduce the list's length by one. Implementations used by
/// [ReorderableListView] will need to account for this when inserting before
/// [newIndex].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE}
///
/// {@tool snippet}
///
/// ```dart
/// final List<MyDataObject> backingList = <MyDataObject>[/* ... */];
///
/// void handleReorder(int oldIndex, int newIndex) {
/// if (oldIndex < newIndex) {
/// // removing the item at oldIndex will shorten the list by 1.
/// newIndex -= 1;
/// }
/// final MyDataObject element = backingList.removeAt(oldIndex);
/// backingList.insert(newIndex, element);
/// }
/// ```
/// {@end-tool}
typedef ReorderCallback = void Function(int oldIndex, int newIndex);
/// A list whose items the user can interactively reorder by dragging.
///
/// This class is appropriate for views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the list view instead of just
/// those children that are actually visible.
///
/// All [children] must have a key.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE}
class LSReorderableListView extends StatefulWidget {
/// Creates a reorderable list.
LSReorderableListView({
Key key,
this.header,
@required this.children,
@required this.onReorder,
this.scrollController,
this.scrollDirection = Axis.vertical,
this.padding,
this.reverse = false,
}) : assert(scrollDirection != null),
assert(onReorder != null),
assert(children != null),
assert(
children.every((Widget w) => w.key != null),
'All children of this widget must have a key.',
),
super(key: key);
/// A non-reorderable header widget to show before the list.
///
/// If null, no header will appear before the list.
final Widget header;
/// The widgets to display.
final List<Widget> children;
/// The [Axis] along which the list scrolls.
///
/// List [children] can only drag along this [Axis].
final Axis scrollDirection;
/// Creates a [ScrollPosition] to manage and determine which portion
/// of the content is visible in the scroll view.
///
/// This can be used in many ways, such as setting an initial scroll offset,
/// (via [ScrollController.initialScrollOffset]), reading the current scroll position
/// (via [ScrollController.offset]), or changing it (via [ScrollController.jumpTo] or
/// [ScrollController.animateTo]).
final ScrollController scrollController;
/// The amount of space by which to inset the [children].
final EdgeInsets padding;
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// Called when a list child is dropped into a new position to shuffle the
/// underlying list.
///
/// This [ReorderableListView] calls [onReorder] after a list child is dropped
/// into a new position.
final ReorderCallback onReorder;
@override
_ReorderableListViewState createState() => _ReorderableListViewState();
}
// This top-level state manages an Overlay that contains the list and
// also any Draggables it creates.
//
// _ReorderableListContent manages the list itself and reorder operations.
//
// The Overlay doesn't properly keep state by building new overlay entries,
// and so we cache a single OverlayEntry for use as the list layer.
// That overlay entry then builds a _ReorderableListContent which may
// insert Draggables into the Overlay above itself.
class _ReorderableListViewState extends State<LSReorderableListView> {
// We use an inner overlay so that the dragging list item doesn't draw outside of the list itself.
final GlobalKey _overlayKey = GlobalKey(debugLabel: 'modded $ReorderableListView overlay key');
// This entry contains the scrolling list itself.
OverlayEntry _listOverlayEntry;
@override
void initState() {
super.initState();
_listOverlayEntry = OverlayEntry(
opaque: true,
builder: (BuildContext context) {
return _ReorderableListContent(
header: widget.header,
children: widget.children,
scrollController: widget.scrollController,
scrollDirection: widget.scrollDirection,
onReorder: widget.onReorder,
padding: widget.padding,
reverse: widget.reverse,
);
},
);
}
@override
Widget build(BuildContext context) {
return Overlay(
key: _overlayKey,
initialEntries: <OverlayEntry>[
_listOverlayEntry,
]);
}
}
// This widget is responsible for the inside of the Overlay in the
// ReorderableListView.
class _ReorderableListContent extends StatefulWidget {
const _ReorderableListContent({
@required this.header,
@required this.children,
@required this.scrollController,
@required this.scrollDirection,
@required this.padding,
@required this.onReorder,
@required this.reverse,
});
final Widget header;
final List<Widget> children;
final ScrollController scrollController;
final Axis scrollDirection;
final EdgeInsets padding;
final ReorderCallback onReorder;
final bool reverse;
@override
_ReorderableListContentState createState() => _ReorderableListContentState();
}
class _ReorderableListContentState extends State<_ReorderableListContent> with TickerProviderStateMixin<_ReorderableListContent> {
// The extent along the [widget.scrollDirection] axis to allow a child to
// drop into when the user reorders list children.
//
// This value is used when the extents haven't yet been calculated from
// the currently dragging widget, such as when it first builds.
static const double _defaultDropAreaExtent = 8.0;
// The additional margin to place around a computed drop area.
static const double _dropAreaMargin = 8.0;
// How long an animation to reorder an element in the list takes.
static const Duration _reorderAnimationDuration = Duration(milliseconds: 200);
// How long an animation to scroll to an off-screen element in the
// list takes.
static const Duration _scrollAnimationDuration = Duration(milliseconds: 200);
// Controls scrolls and measures scroll progress.
ScrollController _scrollController;
// This controls the entrance of the dragging widget into a new place.
AnimationController _entranceController;
// This controls the 'ghost' of the dragging widget, which is left behind
// where the widget used to be.
AnimationController _ghostController;
// The member of widget.children currently being dragged.
//
// Null if no drag is underway.
Key _dragging;
// The last computed size of the feedback widget being dragged.
Size _draggingFeedbackSize;
// The location that the dragging widget occupied before it started to drag.
int _dragStartIndex = 0;
// The index that the dragging widget most recently left.
// This is used to show an animation of the widget's position.
int _ghostIndex = 0;
// The index that the dragging widget currently occupies.
int _currentIndex = 0;
// The widget to move the dragging widget too after the current index.
int _nextIndex = 0;
// Whether or not we are currently scrolling this view to show a widget.
bool _scrolling = false;
double get _dropAreaExtent {
if (_draggingFeedbackSize == null) {
return _defaultDropAreaExtent;
}
double dropAreaWithoutMargin;
switch (widget.scrollDirection) {
case Axis.horizontal:
dropAreaWithoutMargin = _draggingFeedbackSize.width;
break;
case Axis.vertical:
default:
dropAreaWithoutMargin = _draggingFeedbackSize.height;
break;
}
return dropAreaWithoutMargin + _dropAreaMargin;
}
@override
void initState() {
super.initState();
_entranceController = AnimationController(vsync: this, duration: _reorderAnimationDuration);
_ghostController = AnimationController(vsync: this, duration: _reorderAnimationDuration);
_entranceController.addStatusListener(_onEntranceStatusChanged);
}
@override
void didChangeDependencies() {
_scrollController = widget.scrollController ?? PrimaryScrollController.of(context) ?? ScrollController();
super.didChangeDependencies();
}
@override
void dispose() {
_entranceController.dispose();
_ghostController.dispose();
super.dispose();
}
// Animates the droppable space from _currentIndex to _nextIndex.
void _requestAnimationToNextIndex() {
if (_entranceController.isCompleted) {
_ghostIndex = _currentIndex;
if (_nextIndex == _currentIndex) {
return;
}
_currentIndex = _nextIndex;
_ghostController.reverse(from: 1.0);
_entranceController.forward(from: 0.0);
}
}
// Requests animation to the latest next index if it changes during an animation.
void _onEntranceStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {
_requestAnimationToNextIndex();
});
}
}
// Scrolls to a target context if that context is not on the screen.
void _scrollTo(BuildContext context) {
if (_scrolling)
return;
final RenderObject contextObject = context.findRenderObject();
final RenderAbstractViewport viewport = RenderAbstractViewport.of(contextObject);
assert(viewport != null);
// If and only if the current scroll offset falls in-between the offsets
// necessary to reveal the selected context at the top or bottom of the
// screen, then it is already on-screen.
final double margin = _dropAreaExtent;
final double scrollOffset = _scrollController.offset;
final double topOffset = max(
_scrollController.position.minScrollExtent,
viewport.getOffsetToReveal(contextObject, 0.0).offset - margin,
);
final double bottomOffset = min(
_scrollController.position.maxScrollExtent,
viewport.getOffsetToReveal(contextObject, 1.0).offset + margin,
);
final bool onScreen = scrollOffset <= topOffset && scrollOffset >= bottomOffset;
// If the context is off screen, then we request a scroll to make it visible.
if (!onScreen) {
_scrolling = true;
_scrollController.position.animateTo(
scrollOffset < bottomOffset ? bottomOffset : topOffset,
duration: _scrollAnimationDuration,
curve: Curves.easeInOut,
).then((void value) {
setState(() {
_scrolling = false;
});
});
}
}
// Wraps children in Row or Column, so that the children flow in
// the widget's scrollDirection.
Widget _buildContainerForScrollDirection({ List<Widget> children }) {
switch (widget.scrollDirection) {
case Axis.horizontal:
return Row(children: children);
case Axis.vertical:
default:
return Column(children: children);
}
}
// Wraps one of the widget's children in a DragTarget and Draggable.
// Handles up the logic for dragging and reordering items in the list.
Widget _wrap(Widget toWrap, int index, BoxConstraints constraints) {
assert(toWrap.key != null);
final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key);
// We pass the toWrapWithGlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
// Starts dragging toWrap.
void onDragStarted() {
setState(() {
_dragging = toWrap.key;
_dragStartIndex = index;
_ghostIndex = index;
_currentIndex = index;
_entranceController.value = 1.0;
_draggingFeedbackSize = keyIndexGlobalKey.currentContext.size;
});
}
// Places the value from startIndex one space before the element at endIndex.
void reorder(int startIndex, int endIndex) {
setState(() {
if (startIndex != endIndex)
widget.onReorder(startIndex, endIndex);
// Animates leftover space in the drop area closed.
_ghostController.reverse(from: 0.1);
_entranceController.reverse(from: 0.1);
_dragging = null;
});
}
// Drops toWrap into the last position it was hovering over.
void onDragEnded() {
reorder(_dragStartIndex, _currentIndex);
}
Widget wrapWithSemantics() {
// First, determine which semantics actions apply.
final Map<CustomSemanticsAction, VoidCallback> semanticsActions = <CustomSemanticsAction, VoidCallback>{};
// Create the appropriate semantics actions.
void moveToStart() => reorder(index, 0);
void moveToEnd() => reorder(index, widget.children.length);
void moveBefore() => reorder(index, index - 1);
// To move after, we go to index+2 because we are moving it to the space
// before index+2, which is after the space at index+1.
void moveAfter() => reorder(index, index + 2);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
// If the item can move to before its current position in the list.
if (index > 0) {
semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToStart)] = moveToStart;
String reorderItemBefore = localizations.reorderItemUp;
if (widget.scrollDirection == Axis.horizontal) {
reorderItemBefore = Directionality.of(context) == TextDirection.ltr
? localizations.reorderItemLeft
: localizations.reorderItemRight;
}
semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore;
}
// If the item can move to after its current position in the list.
if (index < widget.children.length - 1) {
String reorderItemAfter = localizations.reorderItemDown;
if (widget.scrollDirection == Axis.horizontal) {
reorderItemAfter = Directionality.of(context) == TextDirection.ltr
? localizations.reorderItemRight
: localizations.reorderItemLeft;
}
semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter;
semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd;
}
// We pass toWrap with a GlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
//
// We also apply the relevant custom accessibility actions for moving the item
// up, down, to the start, and to the end of the list.
return KeyedSubtree(
key: keyIndexGlobalKey,
child: MergeSemantics(
child: Semantics(
customSemanticsActions: semanticsActions,
child: toWrap,
),
),
);
}
Widget buildDragTarget(BuildContext context, List<Key> acceptedCandidates, List<dynamic> rejectedCandidates) {
final Widget toWrapWithSemantics = wrapWithSemantics();
// We build the draggable inside of a layout builder so that we can
// constrain the size of the feedback dragging widget.
Widget child = LongPressDraggable<Key>(
maxSimultaneousDrags: 1,
axis: widget.scrollDirection,
data: toWrap.key,
ignoringFeedbackSemantics: false,
feedback: Container(
alignment: Alignment.topLeft,
// These constraints will limit the cross axis of the drawn widget.
constraints: constraints,
child: Material(
elevation: 6.0,
child: toWrapWithSemantics,
),
),
child: _dragging == toWrap.key ? const SizedBox() : toWrapWithSemantics,
childWhenDragging: const SizedBox(),
dragAnchor: DragAnchor.child,
onDragStarted: onDragStarted,
// When the drag ends inside a DragTarget widget, the drag
// succeeds, and we reorder the widget into position appropriately.
onDragCompleted: onDragEnded,
// When the drag does not end inside a DragTarget widget, the
// drag fails, but we still reorder the widget to the last position it
// had been dragged to.
onDraggableCanceled: (Velocity velocity, Offset offset) {
onDragEnded();
},
);
// The target for dropping at the end of the list doesn't need to be
// draggable.
if (index >= widget.children.length) {
child = toWrap;
}
// Determine the size of the drop area to show under the dragging widget.
Widget spacing;
switch (widget.scrollDirection) {
case Axis.horizontal:
spacing = SizedBox(width: _dropAreaExtent);
break;
case Axis.vertical:
default:
spacing = SizedBox(height: _dropAreaExtent);
break;
}
// We open up a space under where the dragging widget currently is to
// show it can be dropped.
if (_currentIndex == index) {
return _buildContainerForScrollDirection(children: <Widget>[
SizeTransition(
sizeFactor: _entranceController,
axis: widget.scrollDirection,
child: spacing,
),
child,
]);
}
// We close up the space under where the dragging widget previously was
// with the ghostController animation.
if (_ghostIndex == index) {
return _buildContainerForScrollDirection(children: <Widget>[
SizeTransition(
sizeFactor: _ghostController,
axis: widget.scrollDirection,
child: spacing,
),
child,
]);
}
return child;
}
// We wrap the drag target in a Builder so that we can scroll to its specific context.
return Builder(builder: (BuildContext context) {
return DragTarget<Key>(
builder: buildDragTarget,
onWillAccept: (Key toAccept) {
setState(() {
_nextIndex = index;
_requestAnimationToNextIndex();
});
_scrollTo(context);
// If the target is not the original starting point, then we will accept the drop.
return _dragging == toAccept && toAccept != toWrap.key;
},
onAccept: (Key accepted) { },
onLeave: (Object leaving) { },
);
});
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
// We use the layout builder to constrain the cross-axis size of dragging child widgets.
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
const Key endWidgetKey = Key('DraggableList - End Widget');
Widget finalDropArea;
switch (widget.scrollDirection) {
case Axis.horizontal:
finalDropArea = SizedBox(
key: endWidgetKey,
width: _defaultDropAreaExtent,
height: constraints.maxHeight,
);
break;
case Axis.vertical:
default:
finalDropArea = SizedBox(
key: endWidgetKey,
height: _defaultDropAreaExtent,
width: constraints.maxWidth,
);
break;
}
return SingleChildScrollView(
scrollDirection: widget.scrollDirection,
padding: widget.padding,
controller: _scrollController,
reverse: widget.reverse,
physics: AlwaysScrollableScrollPhysics(),
child: _buildContainerForScrollDirection(
children: <Widget>[
if (widget.reverse) _wrap(finalDropArea, widget.children.length, constraints),
if (widget.header != null) widget.header,
for (int i = 0; i < widget.children.length; i += 1) _wrap(widget.children[i], i, constraints),
if (!widget.reverse) _wrap(finalDropArea, widget.children.length, constraints),
],
),
);
});
}
}

View File

@@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
class LSListViewStickyHeader extends StatelessWidget {
final List<Widget> slivers;
final ScrollController controller;
final EdgeInsets padding;
LSListViewStickyHeader({
@required this.slivers,
@required this.controller,
this.padding,
});
@override
Widget build(BuildContext context) => Scrollbar(
controller: controller,
child: Container(
child: CustomScrollView(
controller: controller,
slivers: [
...slivers,
],
physics: AlwaysScrollableScrollPhysics(),
),
padding: padding != null ? padding : EdgeInsets.only(
top: 0.0,
bottom: (MediaQuery.of(context).padding.bottom),
),
),
);
}

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:lunasea/core.dart';
class LSLoader extends StatelessWidget {
final double size;
final Color color;
LSLoader({
Key key,
this.size = 25.0,
this.color,
}): super(key: key);
@override
Widget build(BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SpinKitThreeBounce(
color: color != null ? color : LunaColours.accent,
size: size,
),
],
);
}

View File

@@ -1,3 +0,0 @@
export 'messages/error_message.dart';
export 'messages/generic.dart';
export 'messages/not_enabled.dart';

View File

@@ -1,25 +0,0 @@
import 'package:flutter/material.dart';
import './generic.dart';
class LSErrorMessage extends StatelessWidget {
final Function onTapHandler;
final bool hideButton;
final String message;
final String btnMessage;
LSErrorMessage({
Key key,
@required this.onTapHandler,
this.message = 'An Error Has Occurred',
this.btnMessage = 'Try Again',
this.hideButton = false,
}) : super(key: key);
@override
Widget build(BuildContext context) => LSGenericMessage(
text: message,
showButton: !hideButton,
buttonText: btnMessage,
onTapHandler: onTapHandler,
);
}

View File

@@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSGenericMessage extends StatelessWidget {
final String text;
final String buttonText;
final bool showButton;
final Function onTapHandler;
LSGenericMessage({
Key key,
@required this.text,
this.showButton = false,
this.buttonText = '',
this.onTapHandler,
}) : super(key: key);
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [
LunaDatabaseValue.THEME_AMOLED.key,
LunaDatabaseValue.THEME_AMOLED_BORDER.key,
]),
builder: (context, box, child) => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Card(
child: child,
margin: LunaUI.MARGIN_CARD,
elevation: LunaUI.ELEVATION,
shape: LunaUI.shapeBorder,
),
if(showButton) LunaButtonContainer(
children: [
LunaButton.text(
text: buttonText,
onTap: onTapHandler,
backgroundColor: LunaColours.accent,
),
],
),
],
),
child: Row(
children: <Widget>[
Expanded(
child: Container(
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_TITLE,
),
),
margin: EdgeInsets.symmetric(vertical: 24.0, horizontal: 12.0),
),
),
],
),
);
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSNotEnabled extends StatelessWidget {
final String service;
final bool showButton;
LSNotEnabled(this.service, {
this.showButton = true,
});
@override
Widget build(BuildContext context) => LSGenericMessage(
text: service == Constants.NO_SERVICES_ENABLED
? 'No Modules Enabled'
: '$service Is Not Enabled',
showButton: showButton,
buttonText: 'Return to Dashboard',
onTapHandler: () async => await Navigator.of(context).pushNamedAndRemoveUntil('/', (Route<dynamic> route) => false),
);
}

View File

@@ -1 +0,0 @@
export 'navigation_bar/navigation_bar.dart';

View File

@@ -1,59 +0,0 @@
import 'package:google_nav_bar/google_nav_bar.dart';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSBottomNavigationBar extends StatelessWidget {
final int index;
final Function onTap;
final List<IconData> icons;
final List<String> titles;
final List<Widget> leadingOnGButton;
LSBottomNavigationBar({
@required this.index,
@required this.onTap,
@required this.icons,
@required this.titles,
this.leadingOnGButton,
});
@override
Widget build(BuildContext context) => Container(
child: SafeArea(
top: false,
child: Padding(
child: GNav(
gap: 8.0,
iconSize: 24.0,
padding: EdgeInsets.fromLTRB(18.0, 10.0, 12.0, 10.0),
duration: Duration(milliseconds: LunaUI.ANIMATION_SPEED),
tabBackgroundColor: Theme.of(context).canvasColor,
activeColor: LunaColours.accent,
tabs: List.generate(
icons.length,
(index) => GButton(
icon: icons[index],
text: titles[index],
iconSize: 22.0,
haptic: true,
iconColor: Colors.white,
textStyle: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
color: LunaColours.accent,
),
leading: leadingOnGButton == null ? null : leadingOnGButton[index],
)
).toList(),
tabActiveBorder: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data ? Border.all(color: Colors.white12) : null,
selectedIndex: index,
onTabChange: onTap,
),
padding: EdgeInsets.all(12.0),
),
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
);
}

View File

@@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
// ignore: non_constant_identifier_names
Widget LSNetworkImage({
@required double height,
@required double width,
@required String url,
@required String placeholder,
Map<String, String> headers,
bool roundCorners = true,
}) => ClipRRect(
child: Container(
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset(placeholder),
if(url != null && url.isNotEmpty) FadeInImage(
height: height,
width: width,
fadeInDuration: Duration(milliseconds: LunaUI.ANIMATION_IMAGE_FADE_IN_SPEED),
fadeOutDuration: Duration(milliseconds: 1),
placeholder: AssetImage(placeholder),
fit: BoxFit.cover,
image: NetworkImage(
url,
headers: headers,
),
imageErrorBuilder: (context, error, stack) => Container(
height: height,
width: width,
),
),
],
),
height: height,
width: width,
),
borderRadius: roundCorners
? BorderRadius.circular(Constants.UI_BORDER_RADIUS)
: null,
);

View File

@@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
class LSRefreshIndicator extends StatefulWidget {
final Widget child;
final Function onRefresh;
final GlobalKey<RefreshIndicatorState> refreshKey;
LSRefreshIndicator({
@required this.child,
@required this.onRefresh,
@required this.refreshKey,
});
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LSRefreshIndicator> {
@override
Widget build(BuildContext context) {
return RefreshIndicator(
key: widget.refreshKey,
backgroundColor: Theme.of(context).primaryColor,
onRefresh: widget.onRefresh,
child: widget.child,
);
}
}

View File

@@ -1,90 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flash/flash.dart';
import 'package:lunasea/core.dart';
enum SNACKBAR_TYPE {
success,
failure,
info,
}
const _LEFT_PADDING = 8.0;
// ignore: non_constant_identifier_names
Future<void> LSSnackBar({
@required BuildContext context,
@required String title,
String message,
Duration duration,
SNACKBAR_TYPE type = SNACKBAR_TYPE.info,
bool showButton = false,
String buttonText = 'view',
Function buttonOnPressed,
}) async {
if(showButton) assert(buttonOnPressed != null);
Color color;
IconData icon;
switch(type) {
case SNACKBAR_TYPE.failure: color = LunaColours.red; icon = Icons.error_outline; break;
case SNACKBAR_TYPE.success: color = LunaColours.accent; icon = Icons.check_circle_outline; break;
case SNACKBAR_TYPE.info: color = LunaColours.blue; icon = Icons.info_outline; break;
}
showFlash(
context: context,
duration: duration != null
? duration
: showButton
? Duration(seconds: 4)
: Duration(seconds: 2),
builder: (context, controller) => Flash(
backgroundColor: Theme.of(context).primaryColor,
controller: controller,
style: FlashStyle.floating,
boxShadows: [BoxShadow(blurRadius: 6.0, spreadRadius: 4.0, color: Colors.black.withOpacity(0.10))],
horizontalDismissDirection: HorizontalDismissDirection.horizontal,
margin: EdgeInsets.all(8.0),
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
borderColor: LunaDatabaseValue.THEME_AMOLED.data && LunaDatabaseValue.THEME_AMOLED_BORDER.data
? Colors.white12
: Colors.transparent,
child: FlashBar(
title: Text(
title,
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_TITLE,
),
),
message: Text(
message ?? LunaLogger.checkLogsMessage,
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
),
icon: Padding(
child: LSIconButton(
icon: icon,
color: color,
),
padding: EdgeInsets.only(left: _LEFT_PADDING),
),
primaryAction: showButton
? TextButton(
child: Text(
buttonText.toUpperCase(),
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
color: LunaColours.accent,
),
),
onPressed: () {
controller.dismiss();
buttonOnPressed();
},
)
: null,
),
),
);
}

View File

@@ -1,2 +0,0 @@
export 'table/block.dart';
export 'table/content.dart';

View File

@@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSTableBlock extends StatelessWidget {
final String title;
final List<Widget> children;
LSTableBlock({
Key key,
this.title,
@required this.children,
}) : super(key: key);
List<Widget> _block({
String title,
@required List<Widget> children,
}) => [
if(title != null) LSHeader(text: title),
LSCard(
child: Padding(
child: Column(
children: children,
),
padding: EdgeInsets.symmetric(vertical: 8.0),
),
),
];
@override
Widget build(BuildContext context) => Column(
children: _block(title: title, children: children),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
);
}

View File

@@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSTableContent extends StatelessWidget {
final String title;
final String body;
final TextAlign titleAlign;
final TextAlign bodyAlign;
final int titleFlex;
final int bodyFlex;
final EdgeInsets padding;
final Color titleColour;
final Color bodyColour;
LSTableContent({
Key key,
@required this.title,
@required this.body,
this.titleAlign = TextAlign.end,
this.bodyAlign = TextAlign.start,
this.titleFlex = 2,
this.bodyFlex = 5,
this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
this.titleColour = Colors.white70,
this.bodyColour = Colors.white,
}) : super(key: key);
@override
Widget build(BuildContext context) => Padding(
child: Row(
children: [
Expanded(
child: Text(
title.toUpperCase(),
textAlign: titleAlign,
style: TextStyle(
color: titleColour,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
),
flex: titleFlex,
),
Container(width: 16.0, height: 0.0),
Expanded(
child: Text(
body,
textAlign: bodyAlign,
style: TextStyle(
color: bodyColour,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
),
flex: bodyFlex,
),
],
crossAxisAlignment: CrossAxisAlignment.start,
),
padding: padding,
);
}

View File

@@ -1,4 +0,0 @@
export 'text/header.dart';
export 'text/text_highlighted.dart';
export 'text/text_title.dart';
export 'text/text_subtitle.dart';

View File

@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSHeader extends StatelessWidget {
final String text;
final String subtitle;
LSHeader({
@required this.text,
this.subtitle,
});
@override
Widget build(BuildContext context) => Padding(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
text.toUpperCase(),
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_HEADER,
color: Colors.white,
),
),
Padding(
child: Container(
height: 2.0,
width: 48.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
color: LunaColours.accent,
),
),
padding: EdgeInsets.only(
top: 4.0,
left: 1.0,
bottom: subtitle != null
? 6.0
: 0.0,
),
),
if(subtitle != null) Text(
subtitle,
style: TextStyle(
fontSize: Constants.UI_FONT_SIZE_SUBHEADER,
color: Colors.white70,
fontWeight: FontWeight.w300,
),
),
],
),
padding: EdgeInsets.fromLTRB(12.0, 6.0, 12.0, 8.0),
);
}

View File

@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSTextHighlighted extends StatelessWidget {
final Color bgColor;
final Color fgColor;
final String text;
final EdgeInsets margin;
LSTextHighlighted({
@required this.text,
this.bgColor = LunaColours.accent,
this.fgColor = Colors.white,
this.margin = const EdgeInsets.only(right: 8.0),
});
@override
Widget build(BuildContext context) => Padding(
child: Container(
child: Padding(
child: Text(
text,
maxLines: 1,
style: TextStyle(
fontSize: Constants.UI_FONT_SIZE_SUBHEADER,
color: fgColor,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
),
),
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
),
),
padding: margin,
);
}

View File

@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSSubtitle extends StatelessWidget {
final String text;
final int maxLines;
final bool darken;
final bool centerText;
final bool softWrap;
final TextOverflow overflow;
LSSubtitle({
@required this.text,
this.maxLines = 1,
this.darken = false,
this.centerText = false,
this.softWrap = false,
this.overflow = TextOverflow.fade,
});
@override
Widget build(BuildContext context) => Text(
text,
overflow: overflow,
softWrap: softWrap,
maxLines: maxLines,
textAlign: centerText
? TextAlign.center
: TextAlign.start,
style: TextStyle(
color: darken ? Colors.white30 : Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
);
}

View File

@@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSTitle extends StatelessWidget {
final String text;
final int maxLines;
final bool darken;
final bool centerText;
final bool softWrap;
LSTitle({
@required this.text,
this.maxLines = 1,
this.softWrap = false,
this.darken = false,
this.centerText = false,
});
@override
Widget build(BuildContext context) => Text(
text,
overflow: TextOverflow.fade,
softWrap: softWrap,
maxLines: maxLines,
textAlign: centerText
? TextAlign.center
: TextAlign.start,
style: TextStyle(
color: darken ? Colors.white30 : Colors.white,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_TITLE,
),
);
}

View File

@@ -2,22 +2,22 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
abstract class LSDialog {
static const HEADER_SIZE = Constants.UI_FONT_SIZE_HEADER;
static const BODY_SIZE = Constants.UI_FONT_SIZE_SUBTITLE;
static const SUBBODY_SIZE = Constants.UI_FONT_SIZE_SUBHEADER;
static const BUTTON_SIZE = Constants.UI_FONT_SIZE_SUBHEADER;
abstract class LunaDialog {
static const HEADER_SIZE = LunaUI.FONT_SIZE_HEADER;
static const BODY_SIZE = LunaUI.FONT_SIZE_SUBTITLE;
static const SUBBODY_SIZE = LunaUI.FONT_SIZE_SUBHEADER;
static const BUTTON_SIZE = LunaUI.FONT_SIZE_SUBHEADER;
static Widget title({ @required String text }) => Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: LSDialog.HEADER_SIZE,
fontSize: LunaDialog.HEADER_SIZE,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
),
);
static TextSpan bolded({ @required String text, double fontSize = LSDialog.BODY_SIZE, Color color }) => TextSpan(
static TextSpan bolded({ @required String text, double fontSize = LunaDialog.BODY_SIZE, Color color }) => TextSpan(
text: text,
style: TextStyle(
color: color == null
@@ -31,7 +31,7 @@ abstract class LSDialog {
static Widget richText({ @required List<TextSpan> children, TextAlign alignment = TextAlign.start }) => RichText(
text: TextSpan(
style: TextStyle(
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
children: children,
),
@@ -45,7 +45,7 @@ abstract class LSDialog {
color: textColor == null
? LunaColours.accent
: textColor,
fontSize: LSDialog.BUTTON_SIZE,
fontSize: LunaDialog.BUTTON_SIZE,
),
),
onPressed: onPressed == null ? null : () async {
@@ -59,7 +59,7 @@ abstract class LSDialog {
text ?? 'lunasea.Cancel'.tr(),
style: TextStyle(
color: textColor,
fontSize: LSDialog.BUTTON_SIZE,
fontSize: LunaDialog.BUTTON_SIZE,
),
),
onPressed: () {
@@ -79,7 +79,7 @@ abstract class LSDialog {
text,
style: TextStyle(
color: Colors.white,
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
textAlign: textAlign,
);
@@ -88,7 +88,7 @@ abstract class LSDialog {
text: text,
style: TextStyle(
color: Colors.white,
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
);
@@ -105,7 +105,7 @@ abstract class LSDialog {
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent),
@@ -116,7 +116,7 @@ abstract class LSDialog {
),
style: TextStyle(
color: Colors.white,
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
cursorColor: LunaColours.accent,
textInputAction: TextInputAction.done,
@@ -141,7 +141,7 @@ abstract class LSDialog {
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LunaColours.accent),
@@ -152,7 +152,7 @@ abstract class LSDialog {
),
style: TextStyle(
color: Colors.white,
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
),
cursorColor: LunaColours.accent,
textInputAction: TextInputAction.done,
@@ -179,7 +179,7 @@ abstract class LSDialog {
title: Text(
text,
style: TextStyle(
fontSize: LSDialog.BODY_SIZE,
fontSize: LunaDialog.BODY_SIZE,
color: Colors.white,
),
),
@@ -231,15 +231,15 @@ abstract class LSDialog {
context: context,
builder: (context) => AlertDialog(
actions: <Widget>[
if(showCancelButton) LSDialog.cancel(
if(showCancelButton) LunaDialog.cancel(
context,
text: cancelButtonText,
textColor: buttons != null ? Colors.white : LunaColours.accent,
),
if(buttons != null) ...buttons,
],
title: LSDialog.title(text: title),
content: customContent ?? LSDialog.content(children: content),
title: LunaDialog.title(text: title),
content: customContent ?? LunaDialog.content(children: content),
contentPadding: contentPadding,
shape: LunaUI.shapeBorder,
),

View File

@@ -68,7 +68,7 @@ class LunaDrawer extends StatelessWidget {
color: currentPage
? LunaColours.accent
: Colors.white,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
onTap: () async {
@@ -86,21 +86,19 @@ class LunaDrawer extends StatelessWidget {
LunaModule.WAKE_ON_LAN.name,
style: TextStyle(
color: Colors.white,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
onTap: () async {
WakeOnLANAPI api = WakeOnLANAPI.fromProfile();
await api.wake()
.then((_) => showLunaSuccessSnackBar(
context: context,
title: 'Machine is Waking Up...',
message: 'Magic packet successfully sent',
))
.catchError((error, stack) {
LunaLogger().error('Failed to wake machine', error, stack);
return showLunaErrorSnackBar(
context: context,
title: 'Failed to Wake Machine',
error: error,
);

View File

@@ -1,3 +1,2 @@
export 'floating_action_button/floating_action_button.dart';
export 'floating_action_button/floating_action_button_animated.dart';
export 'floating_action_button/floating_action_button_extended.dart';

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaFloatingActionButtonAnimated extends StatelessWidget {
final Object heroTag;
final AnimatedIconData icon;
final AnimationController controller;
final Color color;
final Color backgroundColor;
final Function onPressed;
LunaFloatingActionButtonAnimated({
Key key,
@required this.onPressed,
@required this.icon,
@required this.controller,
this.backgroundColor = LunaColours.accent,
this.color = Colors.white,
this.heroTag,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton(
child: AnimatedIcon(
icon: icon,
color: color,
progress: controller,
),
heroTag: heroTag,
onPressed: onPressed,
backgroundColor: backgroundColor,
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaFloatingActionButtonExtended extends StatelessWidget {
final Color color;
final Color backgroundColor;
final IconData icon;
final String label;
final void Function() onPressed;
final Object heroTag;
LunaFloatingActionButtonExtended({
Key key,
@required this.icon,
@required this.label,
@required this.onPressed,
this.backgroundColor = LunaColours.accent,
this.color = Colors.white,
this.heroTag,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton.extended(
icon: Icon(icon, color: color),
onPressed: onPressed,
label: Text(
label,
style: TextStyle(
color: color,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
letterSpacing: 0.35,
),
),
backgroundColor: backgroundColor,
heroTag: heroTag,
);
}
}

View File

@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaTextInputBar extends StatefulWidget {
static const EdgeInsets preferredSizeWidgetMargin = const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0);
static const double appBarHeight = 62.0;
static const EdgeInsets appBarMargin = const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 12.0);
final TextEditingController controller;
final ScrollController scrollController;
final TextInputAction action;
final TextInputType keyboardType;
final Iterable<String> autofillHints;
@@ -19,6 +22,7 @@ class LunaTextInputBar extends StatefulWidget {
LunaTextInputBar({
@required this.controller,
this.scrollController,
this.onChanged,
this.onSubmitted,
this.validator,
@@ -43,7 +47,8 @@ class _State extends State<LunaTextInputBar> {
bool _isFocused = false;
@override
Widget build(BuildContext context) => LSCard(
Widget build(BuildContext context) => LunaCard(
context: context,
margin: widget.margin,
child: FocusScope(
child: Focus(
@@ -56,7 +61,7 @@ class _State extends State<LunaTextInputBar> {
TextStyle get _sharedTextStyle => TextStyle(
color: Colors.white,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
);
InputDecoration get _sharedInputDecoration => InputDecoration(
@@ -64,7 +69,7 @@ class _State extends State<LunaTextInputBar> {
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
suffixIcon: AnimatedOpacity(
child: GestureDetector(
@@ -74,6 +79,7 @@ class _State extends State<LunaTextInputBar> {
size: 24.0,
),
onTap: widget.onChanged == null ? null : () {
widget.scrollController?.lunaAnimateToStart();
widget.controller.text = '';
widget.onChanged('');
}
@@ -104,6 +110,7 @@ class _State extends State<LunaTextInputBar> {
autocorrect: false,
keyboardType: widget.keyboardType,
validator: widget?.validator,
onTap: widget.scrollController?.lunaAnimateToStart,
onChanged: widget.onChanged == null ? null : widget.onChanged,
onFieldSubmitted: widget?.onSubmitted,
);
@@ -119,6 +126,7 @@ class _State extends State<LunaTextInputBar> {
obscureText: widget.obscureText,
autocorrect: false,
keyboardType: widget.keyboardType,
onTap: widget.scrollController?.lunaAnimateToStart,
onChanged: widget.onChanged == null ? null : widget.onChanged,
onSubmitted: widget?.onSubmitted,
);

View File

@@ -1,4 +1,7 @@
export 'list_view/list_view.dart';
export 'list_view/list_view_builder.dart';
export 'list_view/list_view_modal.dart';
export 'list_view/list_view_modal_builder.dart';
export 'list_view/paged_list_view.dart';
export 'list_view/reorderable_list_view.dart';
export 'list_view/reorderable_list_view_builder.dart';

View File

@@ -18,23 +18,15 @@ class LunaListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollStartNotification>(
onNotification: (notification) {
if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus();
return null;
},
child: Scrollbar(
return Scrollbar(
controller: controller,
child: ListView(
controller: controller,
child: ListView(
controller: controller,
children: children,
itemExtent: itemExtent,
padding: padding != null ? padding : EdgeInsets.only(
top: 8.0,
bottom: 8.0+(MediaQuery.of(context).padding.bottom),
),
physics: physics,
),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
itemExtent: itemExtent,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
),
);
}

View File

@@ -23,24 +23,16 @@ class LunaListViewBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollStartNotification>(
onNotification: (notification) {
if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus();
return null;
},
child: Scrollbar(
return Scrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
child: ListView.builder(
controller: controller,
padding: padding != null ? padding : EdgeInsets.only(
top: 8.0,
bottom: 8.0+(MediaQuery.of(context).padding.bottom),
),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
),
);
}

View File

@@ -1,14 +1,21 @@
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
/// [ListView] with a [Scrollbar] that is intended to be used with a modal bottom sheet.
///
/// The [ScrollController] is pulled from the context from the sheet, and cannot be defined.
///
/// By default, the list is shrink-wrapped.
class LunaListViewModal extends StatelessWidget {
final List<Widget> children;
final double itemExtent;
final bool shrinkWrap;
LunaListViewModal({
Key key,
@required this.children,
this.itemExtent,
this.shrinkWrap = true,
}) : super(key: key);
@override
@@ -17,12 +24,11 @@ class LunaListViewModal extends StatelessWidget {
controller: ModalScrollController.of(context),
child: ListView(
controller: ModalScrollController.of(context),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
itemExtent: itemExtent,
padding: EdgeInsets.only(
top: 8.0,
bottom: 8.0+(MediaQuery.of(context).padding.bottom),
),
shrinkWrap: shrinkWrap,
padding: MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
),
);

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
/// [ListView] builder with a [Scrollbar] that is intended to be used with a modal bottom sheet.
///
/// The [ScrollController] is pulled from the context from the sheet, and cannot be defined.
///
/// By default, the list is shrink-wrapped.
class LunaListViewModalBuilder extends StatelessWidget {
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final double itemExtent;
final bool shrinkWrap;
LunaListViewModalBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
this.itemExtent,
this.shrinkWrap = true,
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: ModalScrollController.of(context),
child: ListView.builder(
controller: ModalScrollController.of(context),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: AlwaysScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
shrinkWrap: shrinkWrap,
),
);
}
}

View File

@@ -42,50 +42,45 @@ class _State<T> extends State<LunaPagedListView<T>> {
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollStartNotification>(
onNotification: (notification) {
if(notification.dragDetails != null) FocusManager.instance.primaryFocus?.unfocus();
return null;
},
child: LunaRefreshIndicator(
key: widget.refreshKey,
context: context,
onRefresh: () => Future.sync(() => widget.pagingController.refresh()),
child: Scrollbar(
controller: widget.scrollController,
child: PagedListView<int, T>(
pagingController: widget.pagingController,
scrollController: widget.scrollController,
itemExtent: widget.itemExtent,
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: widget.itemBuilder,
firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(onTap: () => Future.sync(() => widget.pagingController.refresh())),
firstPageProgressIndicatorBuilder: (context) => LunaLoader(),
newPageProgressIndicatorBuilder: (context) => Padding(
child: Container(
alignment: Alignment.center,
height: 40.0,
child: LunaLoader(size: 16.0, useSafeArea: false),
),
padding: EdgeInsets.only(bottom: 16.0),
),
newPageErrorIndicatorBuilder: (context) => LunaIconButton(icon: Icons.error, color: LunaColours.red),
noItemsFoundIndicatorBuilder: (context) => LunaMessage(
text: widget.noItemsFoundMessage,
buttonText: 'lunasea.Refresh'.tr(),
onTap: () => Future.sync(() => widget.pagingController.refresh()),
),
noMoreItemsIndicatorBuilder: (context) => Padding(
child: LunaIconButton(
icon: Icons.check,
color: LunaColours.accent,
),
padding: EdgeInsets.only(bottom: 16.0),
return LunaRefreshIndicator(
key: widget.refreshKey,
context: context,
onRefresh: () => Future.sync(() => widget.pagingController.refresh()),
child: Scrollbar(
controller: widget.scrollController,
child: PagedListView<int, T>(
pagingController: widget.pagingController,
scrollController: widget.scrollController,
itemExtent: widget.itemExtent,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: widget.itemBuilder,
firstPageErrorIndicatorBuilder: (context) => LunaMessage.error(onTap: () => Future.sync(() => widget.pagingController.refresh())),
firstPageProgressIndicatorBuilder: (context) => LunaLoader(),
newPageProgressIndicatorBuilder: (context) => Padding(
child: Container(
alignment: Alignment.center,
height: 40.0,
child: LunaLoader(size: 16.0, useSafeArea: false),
),
padding: EdgeInsets.only(bottom: 16.0),
),
newPageErrorIndicatorBuilder: (context) => LunaIconButton(icon: Icons.error, color: LunaColours.red),
noItemsFoundIndicatorBuilder: (context) => LunaMessage(
text: widget.noItemsFoundMessage,
buttonText: 'lunasea.Refresh'.tr(),
onTap: () => Future.sync(() => widget.pagingController.refresh()),
),
noMoreItemsIndicatorBuilder: (context) => Padding(
child: LunaIconButton(
icon: Icons.check,
color: LunaColours.accent,
),
padding: EdgeInsets.only(bottom: 16.0),
),
padding: widget.padding != null ? widget.padding : EdgeInsets.only(top: 8.0),
physics: AlwaysScrollableScrollPhysics(),
),
padding: widget.padding ?? EdgeInsets.only(top: 8.0),
physics: AlwaysScrollableScrollPhysics(),
),
),
);

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class LunaReorderableListView extends StatelessWidget {
final List<Widget> children;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final void Function(int, int) onReorder;
LunaReorderableListView({
Key key,
@required this.children,
@required this.controller,
@required this.onReorder,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: ReorderableListView(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: children,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
onReorder: onReorder,
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class LunaReorderableListViewBuilder extends StatelessWidget {
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final EdgeInsetsGeometry padding;
final ScrollPhysics physics;
final ScrollController controller;
final void Function(int, int) onReorder;
LunaReorderableListViewBuilder({
Key key,
@required this.itemCount,
@required this.itemBuilder,
@required this.controller,
@required this.onReorder,
this.padding,
this.physics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key) {
assert(itemCount != null);
assert(itemBuilder != null);
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: controller,
child: ReorderableListView.builder(
scrollController: controller,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: padding ?? MediaQuery.of(context).padding.add(EdgeInsets.symmetric(vertical: 8.0)),
physics: physics,
itemCount: itemCount,
itemBuilder: itemBuilder,
onReorder: onReorder,
),
);
}
}

52
lib/core/ui/scaffold.dart Normal file
View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LunaScaffold extends StatelessWidget {
final GlobalKey<ScaffoldState> scaffoldKey;
final PreferredSizeWidget appBar;
final Widget body;
final Widget drawer;
final Widget bottomNavigationBar;
final Widget floatingActionButton;
final bool extendBody;
final bool extendBodyBehindAppBar;
/// Called when [LunaDatabaseValue.ENABLED_PROFILE] has changed. Triggered within the build function.
final void Function(BuildContext) onProfileChange;
LunaScaffold({
Key key,
@required this.scaffoldKey,
this.appBar,
this.body,
this.drawer,
this.bottomNavigationBar,
this.floatingActionButton,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.onProfileChange,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LunaWillPopScope(
scaffoldKey: scaffoldKey,
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, _, __) {
if(onProfileChange != null) onProfileChange(context);
return Scaffold(
key: scaffoldKey,
appBar: appBar,
body: body,
drawer: drawer,
bottomNavigationBar: bottomNavigationBar,
floatingActionButton: floatingActionButton,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
onDrawerChanged: (_) => FocusManager.instance.primaryFocus?.unfocus(),
);
}
),
);
}
}

View File

@@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
class LunaSliverStickyHeader extends SliverStickyHeader {
LunaSliverStickyHeader({
@required Widget header,
@required List<Widget> children,
}) : super(
header: header,
sliver: SliverPadding(
sliver: SliverList(
delegate: SliverChildListDelegate(children),
),
padding: EdgeInsets.symmetric(vertical: 8.0),
),
);
}

View File

@@ -51,25 +51,25 @@ Future<void> showLunaSnackBar({
horizontalDismissDirection: HorizontalDismissDirection.horizontal,
margin: EdgeInsets.all(_PADDING),
position: position,
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS),
borderColor: LunaUI.shouldUseBorder ? Colors.white12 : Colors.transparent,
child: FlashBar(
title: Text(
title,
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_TITLE,
fontSize: LunaUI.FONT_SIZE_TITLE,
),
),
message: Text(
message,
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
),
icon: Padding(
child: LSIconButton(
child: LunaIconButton(
icon: type.icon,
color: type.color,
),

View File

@@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
Future<void> showLunaErrorSnackBar({
// Not needed anymore, but keeping it to prevent breaking all instances for now
BuildContext context,
@required String title,
dynamic error,
String message,

View File

@@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
Future<void> showLunaInfoSnackBar({
// Not needed anymore, but keeping it to prevent breaking all instances for now
BuildContext context,
@required String title,
@required String message,
bool showButton = false,

View File

@@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
Future<void> showLunaSuccessSnackBar({
// Not needed anymore, but keeping it to prevent breaking all instances for now
BuildContext context,
@required String title,
@required String message,
bool showButton = false,

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class LunaWillPopScope extends WillPopScope {
LunaWillPopScope({
@required GlobalKey<ScaffoldState> scaffoldKey,
@required Widget child,
}) : super(
onWillPop: () async {
if(scaffoldKey?.currentState?.hasDrawer ?? false) {
if(scaffoldKey?.currentState?.isDrawerOpen ?? false) return true;
scaffoldKey?.currentState?.openDrawer();
return false;
}
return true;
},
child: child,
);
}

View File

@@ -29,6 +29,7 @@ class LunaChangelog {
context: LunaState.navigatorKey.currentContext,
builder: (context) => Scaffold(
body: LunaListViewModal(
shrinkWrap: false,
children: [
LunaHeader(text: '$version ($buildNumber)', subtitle: changelog.motd),
if((changelog.changesNew?.length ?? 0) != 0) LunaHeader(text: 'New'),

View File

@@ -1,3 +1,2 @@
export 'dashboard/core.dart';
export 'dashboard/routes.dart';
export 'dashboard/widgets.dart';

View File

@@ -1,5 +1,6 @@
export 'core/api.dart';
export 'core/adapters.dart';
export 'core/database.dart';
export 'core/router.dart';
export 'core/state.dart';
export 'core/dialogs.dart';

View File

@@ -16,24 +16,24 @@ extension CalendarStartingTypeExtension on CalendarStartingType {
switch(this) {
case CalendarStartingType.SCHEDULE: return 'Schedule';
case CalendarStartingType.CALENDAR: return 'Calendar';
default: return null;
}
throw Exception('Unknown CalendarStartingType');
}
String get key {
switch(this) {
case CalendarStartingType.SCHEDULE: return 'schedule';
case CalendarStartingType.CALENDAR: return 'calendar';
default: return null;
}
throw Exception('Unknown CalendarStartingType');
}
IconData get icon {
switch(this) {
case CalendarStartingType.SCHEDULE: return Icons.calendar_view_day;
case CalendarStartingType.CALENDAR: return LunaIcons.calendar;
default: return null;
case CalendarStartingType.SCHEDULE: return LunaIcons.calendar;
case CalendarStartingType.CALENDAR: return Icons.calendar_view_day;
}
throw Exception('Unknown CalendarStartingType');
}
CalendarStartingType fromKey(String key) {

View File

@@ -23,10 +23,6 @@ class CalendarAPI {
);
}
void logError(String text, Object error, StackTrace trace) => LunaLogger().error('Home: $text', error, trace);
Future<bool> testConnection() async => true;
Future<Map<DateTime, List>> getUpcoming(DateTime today) async {
Map<DateTime, List> _upcoming = {};
if(
@@ -41,72 +37,70 @@ class CalendarAPI {
sonarr['enabled'] &&
DashboardDatabaseValue.CALENDAR_ENABLE_SONARR.data
) await _getSonarrUpcoming(_upcoming, today);
return _upcoming.isEmpty
? await Future.delayed(Duration(milliseconds: 500), () => {})
: _upcoming;
return _upcoming;
}
Future<void> _getLidarrUpcoming(Map<DateTime, List> map, DateTime today) async {
try {
Map<String, dynamic> _headers = Map<String, dynamic>.from(lidarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${lidarr['host']}/api/v1/',
queryParameters: {
if(lidarr['key'] != '') 'apikey': lidarr['key'],
'start': _startDate(today),
'end': _startDate(today),
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['releaseDate'] ?? '')?.toLocal()?.lunaFloor;
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarLidarrData(
id: entry['id'] ?? 0,
title: entry['artist']['artistName'] ?? 'Unknown Artist',
albumTitle: entry['title'] ?? 'Unknown Album Title',
artistId: entry['artist']['id'] ?? 0,
hasAllFiles: (entry['statistics'] != null ? entry['statistics']['percentOfTracks'] ?? 0 : 0) == 100,
));
map[date] = day;
}
Map<String, dynamic> _headers = Map<String, dynamic>.from(lidarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${lidarr['host']}/api/v1/',
queryParameters: {
if(lidarr['key'] != '') 'apikey': lidarr['key'],
'start': _startDate(today),
'end': _startDate(today),
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['releaseDate'] ?? '')?.toLocal()?.lunaFloor;
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarLidarrData(
id: entry['id'] ?? 0,
title: entry['artist']['artistName'] ?? 'Unknown Artist',
albumTitle: entry['title'] ?? 'Unknown Album Title',
artistId: entry['artist']['id'] ?? 0,
hasAllFiles: (entry['statistics'] != null ? entry['statistics']['percentOfTracks'] ?? 0 : 0) == 100,
));
map[date] = day;
}
}
} catch (error, stack) {
logError('Failed to fetch Lidarr upcoming content', error, stack);
}
return;
}
Future<void> _getRadarrUpcoming(Map<DateTime, List> map, DateTime today) async {
try {
Map<String, dynamic> _headers = Map<String, dynamic>.from(radarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${radarr['host']}/api/',
queryParameters: {
if(radarr['key'] != '') 'apikey': radarr['key'],
'start': _startDate(today),
'end': _endDate(today),
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['physicalRelease'] ?? '')?.toLocal()?.lunaFloor;
if(date != null) {
List day = map[date] ?? [];
Map<String, dynamic> _headers = Map<String, dynamic>.from(radarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${radarr['host']}/api/v3/',
queryParameters: {
if(radarr['key'] != '') 'apikey': radarr['key'],
'start': _startDate(today),
'end': _endDate(today),
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime physicalRelease = DateTime.tryParse(entry['physicalRelease'] ?? '')?.toLocal()?.lunaFloor;
DateTime digitalRelease = DateTime.tryParse(entry['digitalRelease'] ?? '')?.toLocal()?.lunaFloor;
DateTime release;
if(physicalRelease != null || digitalRelease != null) {
if(physicalRelease == null) release = digitalRelease;
if(digitalRelease == null) release = physicalRelease;
if(release == null) release = digitalRelease.isBefore(physicalRelease) ? digitalRelease : physicalRelease;
if(release != null) {
List day = map[release] ?? [];
day.add(CalendarRadarrData(
id: entry['id'] ?? 0,
title: entry['title'] ?? 'Unknown Title',
@@ -115,59 +109,51 @@ class CalendarAPI {
year: entry['year'] ?? 0,
runtime: entry['runtime'] ?? 0,
));
map[date] = day;
map[release] = day;
}
}
}
} catch (error, stack) {
logError('Failed to fetch Radarr upcoming content', error, stack);
}
return;
}
Future<void> _getSonarrUpcoming(Map<DateTime, List> map, DateTime today) async {
try {
Map<String, dynamic> _headers = Map<String, dynamic>.from(sonarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${sonarr['host']}/api/',
queryParameters: {
if(sonarr['key'] != '') 'apikey': sonarr['key'],
'start': _startDate(today),
'end': _endDate(today),
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['airDateUtc'] ?? '')?.toLocal()?.lunaFloor;
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarSonarrData(
id: entry['id'] ?? 0,
seriesID: entry['seriesId'] ?? 0,
title: entry['series']['title'] ?? 'Unknown Series',
episodeTitle: entry['title'] ?? 'Unknown Episode Title',
seasonNumber: entry['seasonNumber'] ?? -1,
episodeNumber: entry['episodeNumber'] ?? -1,
airTime: entry['airDateUtc'] ?? '',
hasFile: entry['hasFile'] ?? false,
fileQualityProfile: entry['hasFile'] ? entry['episodeFile']['quality']['quality']['name'] : '',
));
map[date] = day;
}
Map<String, dynamic> _headers = Map<String, dynamic>.from(sonarr['headers']);
Dio _client = Dio(
BaseOptions(
baseUrl: '${sonarr['host']}/api/',
queryParameters: {
if(sonarr['key'] != '') 'apikey': sonarr['key'],
'start': _startDate(today),
'end': _endDate(today),
},
headers: _headers,
followRedirects: true,
maxRedirects: 5,
),
);
Response response = await _client.get('calendar');
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['airDateUtc'] ?? '')?.toLocal()?.lunaFloor;
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarSonarrData(
id: entry['id'] ?? 0,
seriesID: entry['seriesId'] ?? 0,
title: entry['series']['title'] ?? 'Unknown Series',
episodeTitle: entry['title'] ?? 'Unknown Episode Title',
seasonNumber: entry['seasonNumber'] ?? -1,
episodeNumber: entry['episodeNumber'] ?? -1,
airTime: entry['airDateUtc'] ?? '',
hasFile: entry['hasFile'] ?? false,
fileQualityProfile: entry['hasFile'] ? entry['episodeFile']['quality']['quality']['name'] : '',
));
map[date] = day;
}
}
} catch (error, stack) {
logError('Failed to fetch Sonarr upcoming content', error, stack);
}
return;
}
String _startDate(DateTime today) => DateFormat('y-MM-dd').format(today.subtract(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_PAST.data)));
String _endDate(DateTime today) => DateFormat('y-MM-dd').format(today.add(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data + 1)));
}
}

View File

@@ -1,3 +1,4 @@
export 'data/lidarr.dart';
export 'data/radarr.dart';
export 'data/sonarr.dart';
export 'data/abstract.dart';

View File

@@ -28,7 +28,7 @@ class CalendarLidarrData extends CalendarData {
TextSpan get subtitle => TextSpan(
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
children: <TextSpan>[
TextSpan(
@@ -41,7 +41,7 @@ class CalendarLidarrData extends CalendarData {
text: '\nNot Downloaded',
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
color: Colors.red,
color: LunaColours.red,
),
),
if(hasAllFiles) TextSpan(
@@ -62,7 +62,7 @@ class CalendarLidarrData extends CalendarData {
),
);
Widget trailing(BuildContext context) => LSIconButton(
Widget trailing(BuildContext context) => LunaIconButton(
icon: Icons.search,
onPressed: () async => trailingOnPress(context),
onLongPress: () async => trailingOnLongPress(context),
@@ -71,8 +71,8 @@ class CalendarLidarrData extends CalendarData {
@override
Future<void> trailingOnPress(BuildContext context) async {
await LidarrAPI.from(Database.currentProfileObject).searchAlbums([id])
.then((_) => LSSnackBar(context: context, title: 'Searching...', message: albumTitle))
.catchError((_) => LSSnackBar(context: context, title: 'Failed to Search', message: LunaLogger.checkLogsMessage, type: SNACKBAR_TYPE.failure));
.then((_) => showLunaSuccessSnackBar(title: 'Searching...', message: albumTitle))
.catchError((error) => showLunaErrorSnackBar(title: 'Failed to Search', error: error));
}
@override

View File

@@ -19,26 +19,29 @@ class CalendarRadarrData extends CalendarData {
@required this.runtime,
}): super(id, title);
String get runtimeString => '\t${Constants.TEXT_BULLET}\t${runtime.lunaRuntime()}';
TextSpan get subtitle => TextSpan(
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
children: <TextSpan>[
children: [
TextSpan(
text: '$year$runtimeString',
children: [
TextSpan(text: year.toString()),
TextSpan(text: LunaUI.TEXT_BULLET.lunaPad(1, '\t')),
TextSpan(text: runtime.lunaRuntime()),
],
),
TextSpan(text: '\n'),
if(!hasFile) TextSpan(
text: '\nNot Downloaded',
text: 'Not Downloaded',
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
color: Colors.red,
color: LunaColours.red,
),
),
if(hasFile) TextSpan(
text: '\nDownloaded ($fileQualityProfile)',
text: 'Downloaded ($fileQualityProfile)',
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
color: LunaColours.accent,
@@ -47,21 +50,28 @@ class CalendarRadarrData extends CalendarData {
],
);
@override
String get bannerURI {
return api['enabled']
? (api['host'] as String).endsWith('/')
? '${api['host']}api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}'
: '${api['host']}/api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}'
: '';
if(api['enabled']) {
if((api['host'] as String).endsWith('/')) {
return '${api['host']}api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}';
}
return '${api['host']}/api/MediaCover/$id/fanart-360.jpg?apikey=${api['key']}';
}
return '';
}
@override
Future<void> enterContent(BuildContext context) async => RadarrMoviesDetailsRouter().navigateTo(context, movieId: id);
Widget trailing(BuildContext context) => LSIconButton(
icon: Icons.search,
onPressed: () async => trailingOnPress(context),
onLongPress: () async => trailingOnLongPress(context),
);
@override
Widget trailing(BuildContext context) {
return LunaIconButton(
icon: Icons.search,
onPressed: () async => trailingOnPress(context),
onLongPress: () async => trailingOnLongPress(context),
);
}
@override
Future<void> trailingOnPress(BuildContext context) async => RadarrAPIHelper().automaticSearch(context: context, movieId: id, title: title);

View File

@@ -29,7 +29,7 @@ class CalendarSonarrData extends CalendarData {
TextSpan get subtitle => TextSpan(
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
),
children: <TextSpan>[
TextSpan(
@@ -45,7 +45,7 @@ class CalendarSonarrData extends CalendarData {
text: hasAired ? '\nMissing' : '\nUnaired',
style: TextStyle(
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
color: hasAired ? Colors.red : LunaColours.blue,
color: hasAired ? LunaColours.red : LunaColours.blue,
),
),
if(hasFile) TextSpan(
@@ -77,7 +77,7 @@ class CalendarSonarrData extends CalendarData {
);
Widget trailing(BuildContext context) => InkWell(
child: LSIconButton(
child: LunaIconButton(
text: airTimeString,
onPressed: () async => trailingOnPress(context),
onLongPress: () => trailingOnLongPress(context),
@@ -100,11 +100,9 @@ class CalendarSonarrData extends CalendarData {
@override
Future<void> trailingOnPress(BuildContext context) async {
if(context.read<SonarrState>().api != null) context.read<SonarrState>().api.command.episodeSearch(episodeIds: [id])
.then((_) => LSSnackBar(
context: context,
.then((_) => showLunaSuccessSnackBar(
title: 'Searching for Episode...',
message: episodeTitle,
type: SNACKBAR_TYPE.success,
))
.catchError((error, stack) {
LunaLogger().error(
@@ -112,10 +110,9 @@ class CalendarSonarrData extends CalendarData {
error,
stack,
);
LSSnackBar(
context: context,
showLunaErrorSnackBar(
title: 'Failed to Search',
type: SNACKBAR_TYPE.failure,
error: error,
);
});
}

View File

@@ -13,25 +13,25 @@ class DashboardDialogs {
Navigator.of(context, rootNavigator: true).pop();
}
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: 'Page',
content: List.generate(
DashboardNavigationBar.titles.length,
(index) => LSDialog.tile(
(index) => LunaDialog.tile(
text: DashboardNavigationBar.titles[index],
icon: DashboardNavigationBar.icons[index],
iconColor: LunaColours.list(index),
onTap: () => _setValues(true, index),
),
),
contentPadding: LSDialog.listDialogContentPadding(),
contentPadding: LunaDialog.listDialogContentPadding(),
);
return Tuple2(_flag, _index);
}
static Future<List<dynamic>> setPastDays(BuildContext context) async {
Future<Tuple2<bool, int>> setPastDays(BuildContext context) async {
bool _flag = false;
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
TextEditingController _textController = TextEditingController(text: DashboardDatabaseValue.CALENDAR_DAYS_PAST.data.toString());
@@ -43,20 +43,20 @@ class DashboardDialogs {
}
}
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: 'Past Days',
buttons: [
LSDialog.button(
LunaDialog.button(
text: 'Set',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Set the number of days in the past to fetch calendar entries for.'),
LunaDialog.textContent(text: 'Set the number of days in the past to fetch calendar entries for.'),
Form(
key: _formKey,
child: LSDialog.textFormInput(
child: LunaDialog.textFormInput(
controller: _textController,
title: 'Past Days',
onSubmitted: (_) => _setValues(true),
@@ -69,13 +69,13 @@ class DashboardDialogs {
),
),
],
contentPadding: LSDialog.inputTextDialogContentPadding(),
contentPadding: LunaDialog.inputTextDialogContentPadding(),
);
return [_flag, int.tryParse(_textController.text) ?? 14];
return Tuple2(_flag, int.tryParse(_textController.text) ?? 14);
}
static Future<List<dynamic>> setFutureDays(BuildContext context) async {
Future<Tuple2<bool, int>> setFutureDays(BuildContext context) async {
bool _flag = false;
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
TextEditingController _textController = TextEditingController(text: DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data.toString());
@@ -87,20 +87,20 @@ class DashboardDialogs {
}
}
await LSDialog.dialog(
await LunaDialog.dialog(
context: context,
title: 'Future Days',
buttons: [
LSDialog.button(
LunaDialog.button(
text: 'Set',
onPressed: () => _setValues(true),
),
],
content: [
LSDialog.textContent(text: 'Set the number of days in the future to fetch calendar entries for.'),
LunaDialog.textContent(text: 'Set the number of days in the future to fetch calendar entries for.'),
Form(
key: _formKey,
child: LSDialog.textFormInput(
child: LunaDialog.textFormInput(
controller: _textController,
title: 'Future Days',
onSubmitted: (_) => _setValues(true),
@@ -113,9 +113,9 @@ class DashboardDialogs {
),
),
],
contentPadding: LSDialog.inputTextDialogContentPadding(),
contentPadding: LunaDialog.inputTextDialogContentPadding(),
);
return [_flag, int.tryParse(_textController.text) ?? 14];
return Tuple2(_flag, int.tryParse(_textController.text) ?? 14);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardRouter extends LunaModuleRouter {
@override
void defineAllRoutes(FluroRouter router) {
DashboardHomeRouter().defineRoute(router);
}
}
abstract class DashboardPageRouter extends LunaPageRouter {
DashboardPageRouter(String route) : super(route);
@override
void noParameterRouteDefinition(FluroRouter router, Widget widget, { bool homeRoute = false }) => router.define(
fullRoute,
handler: Handler(handlerFunc: (context, params) => widget),
transitionType: LunaRouter.transitionType,
);
@override
void withParameterRouteDefinition(
FluroRouter router,
Widget Function(BuildContext, Map<String, List<String>>) handlerFunc,
{ bool homeRoute = false }
) => router.define(
fullRoute,
handler: Handler(handlerFunc: (context, params) => handlerFunc(context, params)),
transitionType: LunaRouter.transitionType,
);
}

View File

@@ -2,15 +2,15 @@ import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardState extends LunaModuleState {
@override
void reset() {}
DashboardState() {
reset();
}
int _navigationIndex = DashboardDatabaseValue.NAVIGATION_INDEX.data;
int get navigationIndex => _navigationIndex;
set navigationIndex(int navigationIndex) {
assert(navigationIndex != null);
_navigationIndex = navigationIndex;
notifyListeners();
@override
void reset() {
resetToday();
resetAPI();
resetUpcoming();
}
CalendarStartingType _calendarStartingType = DashboardDatabaseValue.CALENDAR_STARTING_TYPE.data;
@@ -20,4 +20,27 @@ class DashboardState extends LunaModuleState {
_calendarStartingType = calendarStartingType;
notifyListeners();
}
CalendarAPI _api;
CalendarAPI get api => _api;
void resetAPI() {
ProfileHiveObject _profile = Database.currentProfileObject;
_api = CalendarAPI.from(_profile);
notifyListeners();
}
DateTime _today;
DateTime get today => _today;
void resetToday() {
_today = DateTime.now();
notifyListeners();
}
Future<Map<DateTime, List>> _upcoming;
Future<Map<DateTime, List>> get upcoming => _upcoming;
void resetUpcoming() {
if(_api != null) _upcoming = _api.getUpcoming(DateTime.now());
notifyListeners();
}
}

View File

@@ -1,3 +1,3 @@
export 'routes/calendar.dart';
export 'routes/quick_access.dart';
export 'routes/home.dart';
export 'routes/dashboard.dart';
export 'routes/modules.dart';

View File

@@ -1,69 +1,2 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardCalendar extends StatefulWidget {
static const ROUTE_NAME = '/dashboard/calendar';
final CalendarAPI api = CalendarAPI.from(Database.currentProfileObject);
final GlobalKey<RefreshIndicatorState> refreshIndicatorKey;
DashboardCalendar({
Key key,
@required this.refreshIndicatorKey,
}) : super(key: key);
@override
State<DashboardCalendar> createState() => _State();
}
class _State extends State<DashboardCalendar> with AutomaticKeepAliveClientMixin {
DateTime _today;
Future<Map<DateTime, List>> _future;
Map<DateTime, List> _events;
@override
bool get wantKeepAlive => true;
@override
initState() {
super.initState();
_refresh();
}
Future<void> _refresh() async {
_today = DateTime.now().lunaFloor;
setState(() {
_future = widget.api.getUpcoming(_today);
});
}
@override
Widget build(BuildContext context) {
super.build(context);
return LSRefreshIndicator(
refreshKey: widget.refreshIndicatorKey,
onRefresh: _refresh,
child: FutureBuilder(
future: _future,
builder: (context, snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.done: {
if(snapshot.hasError || snapshot.data == null) return LSErrorMessage(onTapHandler: () => _refresh());
_events = snapshot.data;
return _list;
}
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
default: return LSLoader();
}
},
),
);
}
Widget get _list => DashboardCalendarWidget(
events: _events,
today: _today,
);
}
export 'calendar/route.dart';
export 'calendar/widgets.dart';

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardCalendarRoute extends StatefulWidget {
@override
State<DashboardCalendarRoute> createState() => _State();
}
class _State extends State<DashboardCalendarRoute> with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin {
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
@override
bool get wantKeepAlive => true;
Future<void> loadCallback() async {
context.read<DashboardState>().resetUpcoming();
}
@override
Widget build(BuildContext context) {
super.build(context);
return LunaRefreshIndicator(
context: context,
key: _refreshKey,
onRefresh: loadCallback,
child: FutureBuilder(
future: context.watch<DashboardState>().upcoming,
builder: (context, snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) LunaLogger().error(
'Failed to fetch unified calendar data',
snapshot.error,
snapshot.stackTrace,
);
return LunaMessage.error(onTap: _refreshKey.currentState?.show);
}
if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) return DashboardCalendarWidget(
events: snapshot.data,
);
return LunaLoader();
},
),
);
}
}

View File

@@ -0,0 +1 @@
export 'widgets/calendar.dart';

View File

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
import 'package:table_calendar/table_calendar.dart';
class DashboardCalendarWidget extends StatefulWidget {
final Map<DateTime, List> events;
DashboardCalendarWidget({
Key key,
@required this.events,
}) : super(key: key);
@override
State<DashboardCalendarWidget> createState() => _State();
}
class _State extends State<DashboardCalendarWidget> {
DateTime _today;
final TextStyle dayTileStyle = TextStyle(
color: Colors.white,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
);
final TextStyle outsideDayTileStyle = TextStyle(
color: Colors.white54,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
);
final TextStyle unavailableTitleStyle = TextStyle(
color: Colors.white12,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
);
final TextStyle weekdayTitleStyle = TextStyle(
color: LunaColours.accent,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: LunaUI.FONT_SIZE_SUBTITLE,
);
List _selectedEvents;
CalendarController _calendarController;
@override
void initState() {
super.initState();
_calendarController = CalendarController();
_today = context.read<DashboardState>().today.lunaFloor;
_selectedEvents = widget.events[_today] ?? [];
}
@override
void dispose() {
_calendarController.dispose();
super.dispose();
}
void _onDaySelected(DateTime day, List events, List _) {
HapticFeedback.selectionClick();
setState(() {
_selectedEvents = events;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Selector<DashboardState, CalendarStartingType>(
selector: (_, state) => state.calendarStartingType,
builder: (context, startingType, _) {
if(startingType == CalendarStartingType.CALENDAR) {
return Padding(
child: Column(
children: [
_calendar(),
_calendarList(),
],
),
padding: EdgeInsets.only(top: 8.0),
);
}
return _schedule();
},
)
);
}
Widget _calendar() {
return ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [
DashboardDatabaseValue.CALENDAR_STARTING_DAY.key,
DashboardDatabaseValue.CALENDAR_STARTING_SIZE.key,
]),
builder: (context, box, _) {
return LunaCard(
context: context,
child: Padding(
child: TableCalendar(
rowHeight: 48.0,
calendarController: _calendarController,
events: widget.events,
headerVisible: false,
startDay: DateTime.now().subtract(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_PAST.data)),
endDay: DateTime.now().add(Duration(days: DashboardDatabaseValue.CALENDAR_DAYS_FUTURE.data)),
startingDayOfWeek: (DashboardDatabaseValue.CALENDAR_STARTING_DAY.data as CalendarStartingDay).data,
calendarStyle: CalendarStyle(
selectedColor: LunaColours.accent.withOpacity(0.25),
markersMaxAmount: 1,
markersColor: LunaColours.accent,
weekendStyle: dayTileStyle,
weekdayStyle: dayTileStyle,
unavailableStyle: unavailableTitleStyle,
outsideStyle: outsideDayTileStyle,
selectedStyle: dayTileStyle,
outsideWeekendStyle: outsideDayTileStyle,
renderDaysOfWeek: true,
highlightToday: true,
todayColor: LunaColours.primary,
todayStyle: dayTileStyle,
outsideDaysVisible: false,
),
daysOfWeekStyle: DaysOfWeekStyle(
weekendStyle: weekdayTitleStyle,
weekdayStyle: weekdayTitleStyle,
),
headerStyle: HeaderStyle(
titleTextStyle: TextStyle(
fontSize: LunaUI.FONT_SIZE_HEADER,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
),
centerHeaderTitle: true,
formatButtonVisible: false,
leftChevronIcon: Icon(Icons.arrow_back_ios_rounded),
rightChevronIcon: Icon(Icons.arrow_forward_ios_rounded),
),
initialCalendarFormat: (DashboardDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize).data,
availableCalendarFormats: const {
CalendarFormat.month : 'Month', CalendarFormat.twoWeeks : '2 Weeks', CalendarFormat.week : 'Week'},
onDaySelected: _onDaySelected,
),
padding: EdgeInsets.only(top: 16.0, bottom: 8.0),
),
);
},
);
}
Widget _calendarList() {
if((_selectedEvents?.length ?? 0) == 0) return Expanded(
child: LunaListView(
controller: DashboardNavigationBar.scrollControllers[1],
children: [
LunaMessage.inList(text: 'No New Content'),
],
),
);
return Expanded(
child: LunaListView(
controller: DashboardNavigationBar.scrollControllers[1],
children: _selectedEvents.map((event) => _entry(event)).toList(),
padding: MediaQuery.of(context).padding.copyWith(top: 0.0, bottom: 8.0),
),
);
}
Widget _schedule() {
if((widget.events?.length ?? 0) == 0) return LunaListView(
controller: DashboardNavigationBar.scrollControllers[1],
children: [
LunaMessage.inList(text: 'No New Content'),
],
);
return LunaListView(
controller: DashboardNavigationBar.scrollControllers[1],
children: _buildDays().expand((element) => element).toList(),
);
}
List<List<Widget>> _buildDays() {
List<List<Widget>> days = [];
List<DateTime> keys = widget.events.keys.toList();
keys.sort();
for(var key in keys) {
if(key.isAfter(_today.subtract(Duration(days: 1))) && widget.events[key].length > 0) days.add(_day(key));
}
return days;
}
List<Widget> _day(DateTime day) {
List<Widget> listCards = [];
for(int i=0; i<widget.events[day].length; i++) listCards.add(_entry(widget.events[day][i]));
return [
LunaHeader(
text: DateFormat('EEEE / MMMM dd, y').format(day)
),
...listCards,
];
}
Widget _entry(dynamic event) {
Map headers;
switch(event.runtimeType) {
case CalendarLidarrData: headers = Database.currentProfileObject.getLidarr()['headers']; break;
case CalendarRadarrData: headers = Database.currentProfileObject.getRadarr()['headers']; break;
case CalendarSonarrData: headers = Database.currentProfileObject.getSonarr()['headers']; break;
default: headers = {}; break;
}
return LunaListTile(
context: context,
title: LunaText.title(text: event.title),
subtitle: RichText(
text: event.subtitle,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 2,
),
trailing: event.trailing(context),
onTap: () async => event.enterContent(context),
contentPadding: true,
decoration: LunaCardDecoration(
uri: event.bannerURI,
headers: headers,
),
);
}
}

View File

@@ -0,0 +1,2 @@
export 'dashboard/route.dart';
export 'dashboard/widgets.dart';

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardHomeRouter extends DashboardPageRouter {
DashboardHomeRouter() : super('/');
@override
void defineRoute(FluroRouter router) => super.noParameterRouteDefinition(router, _DashboardHomeRoute());
}
class _DashboardHomeRoute extends StatefulWidget {
@override
State<_DashboardHomeRoute> createState() => _State();
}
class _State extends State<_DashboardHomeRoute> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
LunaPageController _pageController;
@override
void initState() {
super.initState();
_pageController = LunaPageController(initialPage: DashboardDatabaseValue.NAVIGATION_INDEX.data);
}
@override
Widget build(BuildContext context) {
return LunaScaffold(
scaffoldKey: _scaffoldKey,
body: _body(),
drawer: _drawer(),
appBar: _appBar(),
bottomNavigationBar: _bottomNavigationBar(),
);
}
Widget _drawer() => LunaDrawer(page: LunaModule.DASHBOARD.key);
Widget _bottomNavigationBar() => DashboardNavigationBar(pageController: _pageController);
Widget _appBar() {
return LunaAppBar(
title: 'LunaSea',
useDrawer: true,
scrollControllers: DashboardNavigationBar.scrollControllers,
pageController: _pageController,
actions: [DashboardAppBarSwitchViewAction()],
);
}
Widget _body() {
return LunaDatabaseValue.ENABLED_PROFILE.listen(
builder: (context, _, __) => PageView(
controller: _pageController,
children: _tabs,
),
);
}
List<Widget> get _tabs => [
DashboardModulesRoute(),
DashboardCalendarRoute(),
];
}

View File

@@ -0,0 +1,2 @@
export 'widgets/navigation_bar.dart';
export 'widgets/appbar_switch_view_action.dart';

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardAppBarSwitchViewAction extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<DashboardState, CalendarStartingType>(
selector: (_, state) => state.calendarStartingType,
builder: (context, view, _) => LunaIconButton(
icon: view.icon,
onPressed: () {
view == CalendarStartingType.CALENDAR
? context.read<DashboardState>().calendarStartingType = CalendarStartingType.SCHEDULE
: context.read<DashboardState>().calendarStartingType = CalendarStartingType.CALENDAR;
},
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class DashboardNavigationBar extends StatelessWidget {
static List<ScrollController> scrollControllers = List.generate(icons.length, (_) => ScrollController());
final PageController pageController;
static const List<String> titles = [
'Modules',
'Calendar',
];
static const List<IconData> icons = [
LunaIcons.modules,
LunaIcons.calendar,
];
DashboardNavigationBar({
Key key,
@required this.pageController,
}): super(key: key);
@override
Widget build(BuildContext context) {
return LunaBottomNavigationBar(
pageController: pageController,
scrollControllers: scrollControllers,
icons: icons,
titles: titles,
);
}
}

View File

@@ -1,105 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
import 'package:tuple/tuple.dart';
class Dashboard extends StatefulWidget {
static const ROUTE_NAME = '/';
@override
State<Dashboard> createState() => _State();
}
class _State extends State<Dashboard> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final _pageController = PageController(initialPage: DashboardDatabaseValue.NAVIGATION_INDEX.data);
final List _refreshKeys = [GlobalKey<RefreshIndicatorState>()];
String _profileState = Database.currentProfileObject.toString();
@override
Widget build(BuildContext context) => WillPopScope(
onWillPop: _willPopScope,
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) {
return ValueListenableBuilder(
valueListenable: Database.profilesBox.listenable(keys: [Database.currentProfile]),
builder: (context, profileBox, widget) {
if(_profileState != Database.currentProfileObject.toString()) _refreshProfile();
return Scaffold(
key: _scaffoldKey,
body: _body,
drawer: _drawer,
appBar: _appBar,
bottomNavigationBar: _bottomNavigationBar,
);
}
);
},
),
);
Future<bool> _willPopScope() async {
if(_scaffoldKey.currentState.isDrawerOpen) {
return true;
} else {
_scaffoldKey.currentState.openDrawer();
return false;
}
}
Widget get _drawer => LunaDrawer(page: LunaModule.DASHBOARD.key);
Widget get _appBar => LunaAppBar(
title: 'LunaSea',
useDrawer: true,
actions: Database.currentProfileObject.anyAutomationEnabled
? <Widget>[
Selector<DashboardState, Tuple2<int, CalendarStartingType>>(
selector: (_, state) => Tuple2(state.navigationIndex, state.calendarStartingType),
builder: (context, state, _) => Visibility(
visible: state.item1 == 1,
child: LSIconButton(
icon: state.item2 == CalendarStartingType.CALENDAR
? CalendarStartingType.SCHEDULE.icon
: CalendarStartingType.CALENDAR.icon,
onPressed: () async {
Provider.of<DashboardState>(context, listen: false).calendarStartingType = state.item2 == CalendarStartingType.CALENDAR
? CalendarStartingType.SCHEDULE
: CalendarStartingType.CALENDAR;
},
),
),
),
]
: null,
);
Widget get _bottomNavigationBar => DashboardNavigationBar(pageController: _pageController);
Widget get _body => PageView(
controller: _pageController,
children: _tabs,
onPageChanged: _onPageChanged,
);
List<Widget> get _tabs => [
DashboardQuickAccess(),
Database.currentProfileObject.anyAutomationEnabled
? DashboardCalendar(refreshIndicatorKey: _refreshKeys[0])
: LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false),
];
void _onPageChanged(int index) => Provider.of<DashboardState>(context, listen: false).navigationIndex = index;
void _refreshProfile() {
_profileState = Database.currentProfileObject.toString();
_refreshAllPages();
}
void _refreshAllPages() {
for(var key in _refreshKeys) {
key?.currentState?.show();
}
}
}

View File

@@ -0,0 +1 @@
export 'modules/route.dart';

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
class DashboardModulesRoute extends StatefulWidget {
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<DashboardModulesRoute> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: _list(),
);
}
Widget _list() {
if(!(Database.indexersBox.length > 0) && !(Database.currentProfileObject.anythingEnabled)) {
return LunaMessage(
text: 'lunasea.NoModulesEnabled'.tr(),
buttonText: 'lunasea.GoToSettings'.tr(),
onTap: LunaModule.SETTINGS.launch,
);
}
return _buildList();
}
Widget _buildList() {
List<Widget> modules = [];
int index = 0;
// Build list of all modules
if(Database.indexersBox.length > 0) {
modules.add(_buildFromLunaModule(LunaModule.SEARCH, index));
index++;
}
Database.currentProfileObject.enabledAutomationModules.forEach((module) {
modules.add(_buildFromLunaModule(module, index));
index++;
});
Database.currentProfileObject.enabledClientModules.forEach((module) {
modules.add(_buildFromLunaModule(module, index));
index++;
});
Database.currentProfileObject.enabledMonitoringModules.forEach((module) {
modules.add(_buildFromLunaModule(module, index));
index++;
});
modules.add(_buildFromLunaModule(LunaModule.SETTINGS, index));
// Build that listview
return LunaListView(
controller: DashboardNavigationBar.scrollControllers[0],
children: modules,
);
}
Widget _buildFromLunaModule(LunaModule module, int listIndex) {
return LunaListTile(
context: context,
title: LunaText.title(text: module.name),
subtitle: LunaText.subtitle(text: module.description),
trailing: LunaIconButton(
icon: module.icon,
color: DashboardDatabaseValue.MODULES_BRAND_COLOURS.data ? module.color : LunaColours.list(listIndex),
),
onTap: module.launch,
);
}
}

View File

@@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules.dart';
class DashboardQuickAccess extends StatefulWidget {
static const ROUTE_NAME = '/dashboard/quickaccess';
final ProfileHiveObject profile = Database.currentProfileObject;
@override
State<DashboardQuickAccess> createState() => _State();
}
class _State extends State<DashboardQuickAccess> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return ValueListenableBuilder(
valueListenable: Database.indexersBox.listenable(),
builder: (context, box, _) => widget.profile.anythingEnabled || Database.indexersBox.length > 0
? _body
: LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false),
);
}
Widget get _body {
bool _hasIndexers = Database.indexersBox.length > 0;
int _serviceCount = widget.profile.enabledModules.length + (_hasIndexers ? 2 : 1);
return LSListViewBuilder(
itemCount: _serviceCount,
itemBuilder: (context, index) {
//Check if indexer search should be included at the top
if(index == 0 && _hasIndexers) return DashboardSummaryTile(
title: LunaModule.SEARCH.name,
subtitle: LunaModule.SEARCH.description,
icon: LunaModule.SEARCH.icon,
index: index,
route: LunaModule.SEARCH.route,
color: LunaModule.SEARCH.color,
);
if(index == _serviceCount-1) return DashboardSummaryTile(
title: LunaModule.SETTINGS.name,
subtitle: LunaModule.SETTINGS.description,
icon: LunaModule.SETTINGS.icon,
index: index,
route: LunaModule.SETTINGS.route,
color: LunaModule.SETTINGS.color,
);
LunaModule data = widget.profile.enabledModules[_hasIndexers ? index-1 : index];
if(data != null) return DashboardSummaryTile(
title: data.name,
subtitle: data.description,
icon: data.icon,
index: index,
route: data.route,
color: data.color,
);
return Container();
},
);
}
}

View File

@@ -1,3 +0,0 @@
export 'widgets/calendar.dart';
export 'widgets/summary_tile.dart';
export 'widgets/navigation_bar.dart';

View File

@@ -1,195 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/dashboard.dart';
import 'package:table_calendar/table_calendar.dart';
class DashboardCalendarWidget extends StatefulWidget {
final Map<DateTime, List> events;
final DateTime today;
DashboardCalendarWidget({
Key key,
@required this.events,
@required this.today,
}) : super(key: key);
@override
State<DashboardCalendarWidget> createState() => _State();
}
class _State extends State<DashboardCalendarWidget> with TickerProviderStateMixin {
final TextStyle dayTileStyle = TextStyle(
color: Colors.white,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
);
final TextStyle outsideDayTileStyle = TextStyle(
color: Colors.white30,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
);
final TextStyle weekdayTitleStyle = TextStyle(
color: LunaColours.accent,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
);
List _selectedEvents;
AnimationController _animationController;
CalendarController _calendarController;
@override
void initState() {
super.initState();
_selectedEvents = widget.events[widget.today] ?? [];
_calendarController = CalendarController();
_animationController = AnimationController( vsync: this, duration: kThemeAnimationDuration);
_animationController?.forward();
}
@override
void dispose() {
_animationController.dispose();
_calendarController.dispose();
super.dispose();
}
void _onDaySelected(DateTime day, List events, List _) {
HapticFeedback.selectionClick();
setState(() {
_selectedEvents = events;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Provider.of<DashboardState>(context).calendarStartingType == CalendarStartingType.CALENDAR
? Padding(
child: Column(
children: [
_calendar,
_list,
],
),
padding: EdgeInsets.only(top: 8.0),
)
: _schedule
);
}
Widget get _calendar => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [
DashboardDatabaseValue.CALENDAR_STARTING_DAY.key,
DashboardDatabaseValue.CALENDAR_STARTING_SIZE.key,
]),
builder: (context, box, _) => LSCard(
child: Padding(
child: TableCalendar(
rowHeight: 48.0,
calendarController: _calendarController,
events: widget.events,
startingDayOfWeek: (DashboardDatabaseValue.CALENDAR_STARTING_DAY.data as CalendarStartingDay).data,
calendarStyle: CalendarStyle(
selectedColor: LunaColours.accent.withOpacity(0.25),
markersMaxAmount: 1,
markersColor: LunaColours.accent,
weekendStyle: dayTileStyle,
weekdayStyle: dayTileStyle,
outsideStyle: outsideDayTileStyle,
selectedStyle: dayTileStyle,
outsideWeekendStyle: outsideDayTileStyle,
renderDaysOfWeek: true,
highlightToday: true,
todayColor: LunaColours.primary,
todayStyle: dayTileStyle,
outsideDaysVisible: false,
),
daysOfWeekStyle: DaysOfWeekStyle(
weekendStyle: weekdayTitleStyle,
weekdayStyle: weekdayTitleStyle,
),
headerStyle: HeaderStyle(
titleTextStyle: TextStyle(
fontSize: Constants.UI_FONT_SIZE_HEADER,
fontWeight: LunaUI.FONT_WEIGHT_BOLD,
),
centerHeaderTitle: true,
formatButtonVisible: false,
leftChevronIcon: Icon(Icons.arrow_back_ios_rounded),
rightChevronIcon: Icon(Icons.arrow_forward_ios_rounded),
),
initialCalendarFormat: (DashboardDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize).data,
availableCalendarFormats: const {
CalendarFormat.month : 'Month', CalendarFormat.twoWeeks : '2 Weeks', CalendarFormat.week : 'Week'},
onDaySelected: _onDaySelected,
),
padding: EdgeInsets.only(bottom: 12.0),
),
),
);
Widget get _list => Expanded(
child: LSListView(
children: _selectedEvents.length == 0
? [LSGenericMessage(text: 'No New Content')]
: _selectedEvents.map((event) => _entry(event)).toList(),
customPadding: EdgeInsets.only(bottom: 8.0),
),
);
Widget get _schedule => LSListView(
children: widget.events.length == 0
? [LSGenericMessage(text: 'No New Content')]
: _days.expand((element) => element).toList(),
);
List<List<Widget>> get _days {
List<List<Widget>> days = [];
List<DateTime> keys = widget.events.keys.toList();
keys.sort();
for(var key in keys) {
if(key.isAfter(widget.today.subtract(Duration(days: 1))) && widget.events[key].length > 0) days.add(_day(key));
}
return days;
}
List<Widget> _day(DateTime day) {
List<Widget> listCards = [];
for(int i=0; i<widget.events[day].length; i++) listCards.add(_entry(widget.events[day][i]));
return [
LSHeader(
text: DateFormat('EEEE / MMMM dd, y').format(day)
),
...listCards,
];
}
Widget _entry(dynamic event) {
Map headers;
switch(event.runtimeType) {
case CalendarLidarrData: headers = Database.currentProfileObject.getLidarr()['headers']; break;
case CalendarRadarrData: headers = Database.currentProfileObject.getRadarr()['headers']; break;
case CalendarSonarrData: headers = Database.currentProfileObject.getSonarr()['headers']; break;
default: headers = {}; break;
}
return LSCardTile(
title: LSTitle(text: event.title),
subtitle: RichText(
text: event.subtitle,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 2,
),
trailing: event.trailing(context),
onTap: () async => event.enterContent(context),
padContent: true,
decoration: LunaCardDecoration(
uri: event.bannerURI,
headers: headers,
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More