mirror of
https://github.com/jagandeepbrar/lunasea.git
synced 2026-08-30 20:24:25 +00:00
(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:
25
CHANGELOG.md
25
CHANGELOG.md
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
)];
|
||||
|
||||
@@ -36,10 +36,7 @@ class LunaBottomActionBar extends StatelessWidget {
|
||||
padding: padding,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export 'card/card.dart';
|
||||
export 'card/card_tile.dart';
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export 'containers/row.dart';
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export 'icon/icon_button.dart';
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export 'listview/listview.dart';
|
||||
export 'listview/listview_builder.dart';
|
||||
export 'listview/listview_reorderable.dart';
|
||||
export 'listview/listview_stickyheader.dart';
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'messages/error_message.dart';
|
||||
export 'messages/generic.dart';
|
||||
export 'messages/not_enabled.dart';
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export 'navigation_bar/navigation_bar.dart';
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export 'table/block.dart';
|
||||
export 'table/content.dart';
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export 'text/header.dart';
|
||||
export 'text/text_highlighted.dart';
|
||||
export 'text/text_title.dart';
|
||||
export 'text/text_subtitle.dart';
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
|
||||
42
lib/core/ui/list_view/list_view_modal_builder.dart
Normal file
42
lib/core/ui/list_view/list_view_modal_builder.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
33
lib/core/ui/list_view/reorderable_list_view.dart
Normal file
33
lib/core/ui/list_view/reorderable_list_view.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/core/ui/list_view/reorderable_list_view_builder.dart
Normal file
39
lib/core/ui/list_view/reorderable_list_view_builder.dart
Normal 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
52
lib/core/ui/scaffold.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
18
lib/core/ui/will_pop_scope.dart
Normal file
18
lib/core/ui/will_pop_scope.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export 'dashboard/core.dart';
|
||||
export 'dashboard/routes.dart';
|
||||
export 'dashboard/widgets.dart';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'data/lidarr.dart';
|
||||
export 'data/radarr.dart';
|
||||
export 'data/sonarr.dart';
|
||||
export 'data/abstract.dart';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
33
lib/modules/dashboard/core/router.dart
Normal file
33
lib/modules/dashboard/core/router.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
46
lib/modules/dashboard/routes/calendar/route.dart
Normal file
46
lib/modules/dashboard/routes/calendar/route.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
lib/modules/dashboard/routes/calendar/widgets.dart
Normal file
1
lib/modules/dashboard/routes/calendar/widgets.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'widgets/calendar.dart';
|
||||
229
lib/modules/dashboard/routes/calendar/widgets/calendar.dart
Normal file
229
lib/modules/dashboard/routes/calendar/widgets/calendar.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
lib/modules/dashboard/routes/dashboard.dart
Normal file
2
lib/modules/dashboard/routes/dashboard.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'dashboard/route.dart';
|
||||
export 'dashboard/widgets.dart';
|
||||
65
lib/modules/dashboard/routes/dashboard/route.dart
Normal file
65
lib/modules/dashboard/routes/dashboard/route.dart
Normal 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(),
|
||||
];
|
||||
}
|
||||
2
lib/modules/dashboard/routes/dashboard/widgets.dart
Normal file
2
lib/modules/dashboard/routes/dashboard/widgets.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'widgets/navigation_bar.dart';
|
||||
export 'widgets/appbar_switch_view_action.dart';
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
1
lib/modules/dashboard/routes/modules.dart
Normal file
1
lib/modules/dashboard/routes/modules.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'modules/route.dart';
|
||||
74
lib/modules/dashboard/routes/modules/route.dart
Normal file
74
lib/modules/dashboard/routes/modules/route.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'widgets/calendar.dart';
|
||||
export 'widgets/summary_tile.dart';
|
||||
export 'widgets/navigation_bar.dart';
|
||||
@@ -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
Reference in New Issue
Block a user