From 6b60fda2fa57cc8051b0572f6edfaba06e84fe32 Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Mon, 24 Aug 2020 04:06:00 -0500 Subject: [PATCH] [TestFlight] v4.0.0+400001 (#240) NEW - [Tautulli] View Activity - [Tautulli] View activity details - [Tautulli] View list of users - [Tautulli] Ability to backup configuration & database - [Tautulli] Ability to delete cache & image cache - [Drawer] Ability to set the initial expanded state of categories (automation, clients, & monitoring) TWEAKS - [UI/Loader] New loading indicator - [Home/Calendar] Separate calendar view type into its own preference - [Home/Calendar] Show no modules enabled message when there are no automation modules enabled, not just no modules overall FIXES - [Profiles] Safeguarded fetching of enabled module --- assets/CustomIcons.ttf | Bin 26600 -> 26860 bytes assets/images/tautulli/nouserthumb.png | Bin 0 -> 10677 bytes lib/core.dart | 4 +- lib/core/configuration/import.dart | 13 +- lib/core/constants.dart | 10 +- lib/core/data.dart | 2 - lib/core/data/future_container.dart | 55 ----- lib/core/database/adapters/indexer.dart | 12 +- lib/core/database/adapters/indexer.g.dart | 7 +- lib/core/database/adapters/lunasea.dart | 9 + lib/core/database/adapters/profile.dart | 87 ++++++-- lib/core/database/adapters/profile.g.dart | 19 +- lib/core/extensions.dart | 1 + lib/core/extensions/duration.dart | 13 ++ lib/core/extensions/integer/bytes.dart | 17 +- lib/core/logger.dart | 1 + lib/core/module_flags.dart | 2 +- lib/core/providers.dart | 38 +++- lib/core/routes.dart | 5 + lib/core/state.dart | 13 ++ lib/core/types.dart | 2 + lib/core/{data => types}/api.dart | 0 lib/core/{ => types}/sorter.dart | 0 lib/core/ui.dart | 1 + lib/core/ui/card/card.dart | 7 +- lib/core/ui/icon/icon_custom.dart | 1 + lib/core/ui/loader.dart | 26 +++ lib/core/ui/messages.dart | 1 - lib/core/ui/messages/loading.dart | 9 - lib/core/ui/navigation/appbar_dropdown.dart | 6 +- lib/core/ui/navigation/drawer.dart | 29 ++- lib/core/ui/snackbar.dart | 2 +- lib/modules.dart | 1 + lib/modules/home/core.dart | 2 +- lib/modules/home/core/adapters.dart | 1 + .../core/adapters/calendar_starting_size.dart | 5 - .../adapters/calendar_starting_size.g.dart | 5 - .../core/adapters/calendar_starting_type.dart | 39 ++++ .../adapters/calendar_starting_type.g.dart | 46 ++++ lib/modules/home/core/database.dart | 4 + lib/modules/home/core/state.dart | 35 +++ lib/modules/home/core/state_global.dart | 19 -- lib/modules/home/routes/calendar.dart | 2 +- lib/modules/home/routes/home.dart | 58 ++--- lib/modules/home/routes/quick_access.dart | 6 +- lib/modules/home/widgets/calendar.dart | 6 +- lib/modules/home/widgets/navigation_bar.dart | 6 +- lib/modules/lidarr/routes/add_details.dart | 2 +- lib/modules/lidarr/routes/catalogue.dart | 2 +- lib/modules/lidarr/routes/details_album.dart | 2 +- lib/modules/lidarr/routes/details_artist.dart | 2 +- lib/modules/lidarr/routes/edit_artist.dart | 2 +- lib/modules/lidarr/routes/history.dart | 2 +- lib/modules/lidarr/routes/lidarr.dart | 1 + lib/modules/lidarr/routes/missing.dart | 2 +- lib/modules/lidarr/routes/search_results.dart | 2 +- .../lidarr/widgets/details_album_list.dart | 2 +- lib/modules/nzbget/routes/history.dart | 2 +- lib/modules/nzbget/routes/nzbget.dart | 1 + lib/modules/nzbget/routes/statistics.dart | 2 +- lib/modules/radarr/core/dialogs.dart | 8 +- lib/modules/radarr/core/state.dart | 120 +++++++++- lib/modules/radarr/core/state/global.dart | 119 ---------- lib/modules/radarr/routes/add_details.dart | 2 +- lib/modules/radarr/routes/add_search.dart | 2 +- lib/modules/radarr/routes/catalogue.dart | 8 +- lib/modules/radarr/routes/details_movie.dart | 6 +- lib/modules/radarr/routes/edit_movie.dart | 2 +- lib/modules/radarr/routes/history.dart | 2 +- lib/modules/radarr/routes/missing.dart | 2 +- lib/modules/radarr/routes/radarr.dart | 7 +- lib/modules/radarr/routes/search_results.dart | 8 +- lib/modules/radarr/routes/upcoming.dart | 2 +- .../radarr/widgets/add_search_bar.dart | 6 +- .../radarr/widgets/catalogue_hide_button.dart | 2 +- .../radarr/widgets/catalogue_search_bar.dart | 4 +- .../widgets/catalogue_sorting_button.dart | 2 +- .../radarr/widgets/catalogue_tile.dart | 4 +- .../radarr/widgets/details_edit_button.dart | 4 +- .../radarr/widgets/movie_navigation_bar.dart | 4 +- .../radarr/widgets/navigation_bar.dart | 6 +- .../radarr/widgets/releases_hide_button.dart | 2 +- .../radarr/widgets/releases_search_bar.dart | 4 +- .../widgets/releases_sorting_button.dart | 2 +- lib/modules/sabnzbd/routes/history.dart | 2 +- lib/modules/sabnzbd/routes/sabnzbd.dart | 1 + lib/modules/sabnzbd/routes/statistics.dart | 2 +- lib/modules/search/core/dialogs.dart | 5 +- lib/modules/search/routes/categories.dart | 2 +- lib/modules/search/routes/results.dart | 2 +- lib/modules/settings/core/dialogs.dart | 30 ++- lib/modules/settings/routes/modules.dart | 6 +- .../automation_lidarr/automation_lidarr.dart | 10 +- .../automation_lidarr_headers.dart | 6 +- .../automation_radarr/automation_radarr.dart | 10 +- .../automation_radarr_headers.dart | 6 +- .../automation_sonarr/automation_sonarr.dart | 12 +- .../automation_sonarr_headers.dart | 6 +- .../clients_nzbget/clients_nzbget.dart | 16 +- .../clients_nzbget_headers.dart | 6 +- .../clients_sabnzbd/clients_sabnzbd.dart | 10 +- .../clients_sabnzbd_headers.dart | 6 +- .../settings/routes/modules/general_home.dart | 1 + .../routes/modules/general_lunasea.dart | 11 + .../routes/modules/general_wake_on_lan.dart | 6 +- .../routes/modules/monitoring_tautulli.dart | 1 + .../monitoring_tautulli.dart | 153 ++++++++++++- .../monitoring_tautulli_headers.dart | 110 ++++++++++ .../general/configuration/restore.dart | 1 + .../widgets/general/profile/add_tile.dart | 1 + .../widgets/general/profile/enabled_tile.dart | 1 + .../widgets/general/profile/rename_tile.dart | 1 + .../settings/widgets/modules/home.dart | 1 + .../widgets/modules/home/start_type_tile.dart | 22 ++ .../settings/widgets/modules/lunasea.dart | 9 +- ...art => appearance_amoled_border_tile.dart} | 0 ..._tile.dart => appearance_amoled_tile.dart} | 0 .../drawer_expand_automation_tile.dart | 19 ++ .../lunasea/drawer_expand_clients_tile.dart | 19 ++ .../drawer_expand_monitoring_tile.dart | 19 ++ ...wser_tile.dart => other_browser_tile.dart} | 0 lib/modules/sonarr/routes/add_details.dart | 2 +- lib/modules/sonarr/routes/catalogue.dart | 2 +- lib/modules/sonarr/routes/details_season.dart | 2 +- lib/modules/sonarr/routes/details_series.dart | 2 +- lib/modules/sonarr/routes/edit_series.dart | 2 +- lib/modules/sonarr/routes/history.dart | 2 +- lib/modules/sonarr/routes/missing.dart | 2 +- lib/modules/sonarr/routes/search_results.dart | 2 +- lib/modules/sonarr/routes/sonarr.dart | 1 + lib/modules/sonarr/routes/upcoming.dart | 2 +- lib/modules/tautulli.dart | 2 + lib/modules/tautulli/core.dart | 5 + lib/modules/tautulli/core/constants.dart | 29 +++ lib/modules/tautulli/core/database.dart | 33 +++ lib/modules/tautulli/core/dialogs.dart | 101 +++++++++ lib/modules/tautulli/core/state.dart | 164 ++++++++++++++ lib/modules/tautulli/core/types.dart | 1 + .../tautulli/core/types/global_settings.dart | 33 +++ lib/modules/tautulli/modules.dart | 7 + lib/modules/tautulli/modules/activity.dart | 2 + .../tautulli/modules/activity/route.dart | 85 ++++++++ .../tautulli/modules/activity/widgets.dart | 2 + .../activity/widgets/activity_status.dart | 18 ++ .../activity/widgets/activity_tile.dart | 206 ++++++++++++++++++ .../tautulli/modules/activity_details.dart | 2 + .../modules/activity_details/route.dart | 55 +++++ .../modules/activity_details/widgets.dart | 2 + .../activity_details/widgets/information.dart | 177 +++++++++++++++ .../widgets/terminate_session.dart | 52 +++++ lib/modules/tautulli/modules/history.dart | 2 + .../tautulli/modules/history/route.dart | 31 +++ .../tautulli/modules/history/widgets.dart | 0 lib/modules/tautulli/modules/logs.dart | 2 + lib/modules/tautulli/modules/logs/route.dart | 37 ++++ .../tautulli/modules/logs/widgets.dart | 6 + .../modules/logs/widgets/logins_tile.dart | 22 ++ .../logs/widgets/newsletters_tile.dart | 22 ++ .../logs/widgets/notifications_tile.dart | 22 ++ .../logs/widgets/plex_media_scanner.dart | 22 ++ .../logs/widgets/plex_media_server_tile.dart | 22 ++ .../modules/logs/widgets/tautulli_tile.dart | 22 ++ lib/modules/tautulli/modules/more.dart | 2 + lib/modules/tautulli/modules/more/route.dart | 43 ++++ .../tautulli/modules/more/widgets.dart | 6 + .../modules/more/widgets/graphs_tile.dart | 22 ++ .../modules/more/widgets/libraries_tile.dart | 22 ++ .../modules/more/widgets/logs_tile.dart | 18 ++ .../more/widgets/recently_added_tile.dart | 22 ++ .../modules/more/widgets/statistics_tile.dart | 22 ++ .../more/widgets/synced_items_tile.dart | 22 ++ lib/modules/tautulli/modules/tautulli.dart | 2 + .../tautulli/modules/tautulli/route.dart | 74 +++++++ .../tautulli/modules/tautulli/widgets.dart | 2 + .../tautulli/widgets/global_settings.dart | 121 ++++++++++ .../tautulli/widgets/navigation_bar.dart | 51 +++++ lib/modules/tautulli/modules/users.dart | 2 + lib/modules/tautulli/modules/users/route.dart | 80 +++++++ .../tautulli/modules/users/widgets.dart | 1 + .../modules/users/widgets/user_tile.dart | 135 ++++++++++++ pubspec.lock | 13 +- pubspec.yaml | 13 +- 182 files changed, 2927 insertions(+), 443 deletions(-) create mode 100644 assets/images/tautulli/nouserthumb.png delete mode 100644 lib/core/data.dart delete mode 100644 lib/core/data/future_container.dart create mode 100644 lib/core/extensions/duration.dart create mode 100644 lib/core/state.dart create mode 100644 lib/core/types.dart rename lib/core/{data => types}/api.dart (100%) rename lib/core/{ => types}/sorter.dart (100%) create mode 100644 lib/core/ui/loader.dart delete mode 100644 lib/core/ui/messages/loading.dart create mode 100644 lib/modules/home/core/adapters/calendar_starting_type.dart create mode 100644 lib/modules/home/core/adapters/calendar_starting_type.g.dart create mode 100644 lib/modules/home/core/state.dart delete mode 100644 lib/modules/home/core/state_global.dart delete mode 100644 lib/modules/radarr/core/state/global.dart create mode 100644 lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli_headers.dart create mode 100644 lib/modules/settings/widgets/modules/home/start_type_tile.dart rename lib/modules/settings/widgets/modules/lunasea/{amoled_border_tile.dart => appearance_amoled_border_tile.dart} (100%) rename lib/modules/settings/widgets/modules/lunasea/{amoled_tile.dart => appearance_amoled_tile.dart} (100%) create mode 100644 lib/modules/settings/widgets/modules/lunasea/drawer_expand_automation_tile.dart create mode 100644 lib/modules/settings/widgets/modules/lunasea/drawer_expand_clients_tile.dart create mode 100644 lib/modules/settings/widgets/modules/lunasea/drawer_expand_monitoring_tile.dart rename lib/modules/settings/widgets/modules/lunasea/{browser_tile.dart => other_browser_tile.dart} (100%) create mode 100644 lib/modules/tautulli.dart create mode 100644 lib/modules/tautulli/core.dart create mode 100644 lib/modules/tautulli/core/constants.dart create mode 100644 lib/modules/tautulli/core/database.dart create mode 100644 lib/modules/tautulli/core/dialogs.dart create mode 100644 lib/modules/tautulli/core/state.dart create mode 100644 lib/modules/tautulli/core/types.dart create mode 100644 lib/modules/tautulli/core/types/global_settings.dart create mode 100644 lib/modules/tautulli/modules.dart create mode 100644 lib/modules/tautulli/modules/activity.dart create mode 100644 lib/modules/tautulli/modules/activity/route.dart create mode 100644 lib/modules/tautulli/modules/activity/widgets.dart create mode 100644 lib/modules/tautulli/modules/activity/widgets/activity_status.dart create mode 100644 lib/modules/tautulli/modules/activity/widgets/activity_tile.dart create mode 100644 lib/modules/tautulli/modules/activity_details.dart create mode 100644 lib/modules/tautulli/modules/activity_details/route.dart create mode 100644 lib/modules/tautulli/modules/activity_details/widgets.dart create mode 100644 lib/modules/tautulli/modules/activity_details/widgets/information.dart create mode 100644 lib/modules/tautulli/modules/activity_details/widgets/terminate_session.dart create mode 100644 lib/modules/tautulli/modules/history.dart create mode 100644 lib/modules/tautulli/modules/history/route.dart create mode 100644 lib/modules/tautulli/modules/history/widgets.dart create mode 100644 lib/modules/tautulli/modules/logs.dart create mode 100644 lib/modules/tautulli/modules/logs/route.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets/logins_tile.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets/newsletters_tile.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets/notifications_tile.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets/plex_media_scanner.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets/plex_media_server_tile.dart create mode 100644 lib/modules/tautulli/modules/logs/widgets/tautulli_tile.dart create mode 100644 lib/modules/tautulli/modules/more.dart create mode 100644 lib/modules/tautulli/modules/more/route.dart create mode 100644 lib/modules/tautulli/modules/more/widgets.dart create mode 100644 lib/modules/tautulli/modules/more/widgets/graphs_tile.dart create mode 100644 lib/modules/tautulli/modules/more/widgets/libraries_tile.dart create mode 100644 lib/modules/tautulli/modules/more/widgets/logs_tile.dart create mode 100644 lib/modules/tautulli/modules/more/widgets/recently_added_tile.dart create mode 100644 lib/modules/tautulli/modules/more/widgets/statistics_tile.dart create mode 100644 lib/modules/tautulli/modules/more/widgets/synced_items_tile.dart create mode 100644 lib/modules/tautulli/modules/tautulli.dart create mode 100644 lib/modules/tautulli/modules/tautulli/route.dart create mode 100644 lib/modules/tautulli/modules/tautulli/widgets.dart create mode 100644 lib/modules/tautulli/modules/tautulli/widgets/global_settings.dart create mode 100644 lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart create mode 100644 lib/modules/tautulli/modules/users.dart create mode 100644 lib/modules/tautulli/modules/users/route.dart create mode 100644 lib/modules/tautulli/modules/users/widgets.dart create mode 100644 lib/modules/tautulli/modules/users/widgets/user_tile.dart diff --git a/assets/CustomIcons.ttf b/assets/CustomIcons.ttf index 05a8c67996736fde370f5d7ad00460f3f632cda7..c8267200e284bcad3544f722219654a4247218ef 100644 GIT binary patch delta 611 zcmYLFKWGzC82{dT$tC8_rfL3M(vnMfNs|hNd&HP5p*j>72OX?~w5dnZB*nxAO(2Cr z*Ai;cTnCZvcGZ9`0TIMWP#2+e>EMt-3Iz+9jNi-R9p8PwzxR$Cej~qskQa&FLjbZ5 z01rwHt9kd+xeY)c0WhUn->&ffl^%fe7@#<)ZCK^Vmk#=HhLfwI;3}}EhzB^yTBFt3 z4kH5c3m-f6t&(-|T>pyw8uD8WtJ9>5dKZ0}ia)U$8xPlJm(BpnWjrw1+1TEtBVEOoqBSaJaSzH^D1#r~o5WNjC}V?1J;^fS!EJ%D+xxYF5Fg1geW8`%_B0-@`fysiZ0 z&gFH3WOCUQ<5JKW<`C#ExdWjLHqv>6U=V>{7BH%Z7d&mjBQtNj=CTkxFmtR4E=7X8 zBsdpW)%R0=E*#x(k_)kHvaTBUapC{q)sP>}27TPr2EjRL3eU%2kc)`akkJLfrw#S* y*ky9}^vWDcf~(q!HYK6OO#mv2n7BvT zUXFngs1}IV%QczD^V@u7;AVaS6k)h+V|g7x|DVJBf>{a3=U`w0i2?ygpWI|Q#*K_J zlP@s#Oio}D*6aa`GBC6;FoRh-K$;7vw1wMFfEWNvy~s#X|301VRfCYKSgH5wMF21VuqbN4aU;5Rsb z<`Muv!6p>o*?xsfp3Vhdkibj&r+~aR>1pswmYb!4yO9xa7;N(ZT#!A$j%^h12Y^Td zoIlzCU=ES`ciSFv=uaGwC*;@<0U{utdz>f0@h5%+_}+fbgCCI7A8%7ntedxw6UH|H zW*XoNJEp9mtO3GyWI+CsnF0M54=9kadR?VU^H<`~P6_FFt470$l<;{V<-sXxKJiCuiRv zjGnywwxEAMe~c63>He1_bl{)U0w<`l4N*C!tg7i!tue}Mm*-XEkU0iJH4f_{)5 z`xF1~egDL_`oD|%JNTcZe^HvDJuwKiKc)Sn`)^d+YJ&eTR;x3fK5pp1?fGdO0}1~t zmLk^Kx1}w!Z@9A!nh&SRMk|K zRF5ki)3~Ch2G=+aSJSlqgTsHI{mDXCW!o74W}JUm{*PYJ^bvd@>A&0v!T0${1O)&T zjvJggc?Aoh4e3P~seqMB znD>z92LfqF5n89ydxk%Jxg`hX;1Oz4EO_eMbnZel0D&HdKpbU-U?|v|XyCjxw)P+@ zIvP;r<`IJ3xq<=?C{;NL!%!$7Wd}%i{xA%k5GVvp zfb2N#@$p2zjM4N4fjmIqwgdxC4piqysS_ZOBU&334d7owp)k)e{%0T%S|bfZMWZf3 zAa~x+#~Ok75J!QZ!}+P5pSJVUvwp^%|9?>#b1#WfaG!TqY%K$;E1A^L)%*$LrX^{V z-DU2j!NR%0r{Cq~=9QS+n2QN8De31IkBj?ym&Wq#)Puwo77L(jeQ>tVAY>xl*A*yziJov{6gY)4~=;-R; zX16m(!~N!LEk7D&m=Y1CF)-cJu;GBK@@iax#qn*x%i^Q$989JqpAYtj(1?@ViMI*N#%eLN?mr3=5gMWQxOE|$%$ZH>!GCM(LV z3Wk$D$oV>`RBMKzMrA?SvggwaP4QZ<+0^0Tu|dJ*^~95n+Eolm6&M2 z@|N1GnXio|OpnROM;v<4_7h)LxoTGlh}Jy#XdEgMT-@rXU*|4XSwsyWwGcYV%Ss_0V zYEn5lYEnOt2HQCfQk%1DYYl9RiB8Ba&(tPSvDcn`1-m@@L5xC+0`0BjWHx(a?O5Ir zC3T;jghA@Z%}@^%wUG8ne8?6KffQgAyxilcLrBrJ+qIL=w5!@PB@+*_Sp{NZx!&{Q z{66Iz95n&mIYbg|c_U6y(GXKXr<=+56oG!iB$1abIMQuVfUfo%F1e#Za%@2pWBp8b z^k(^|-P|W+2x#!}ef0matLIDIOIL&)6pnCDK=?swL$cKW}iFU_xE)!T|4 zFcqC)WU=rY!mrZH?X#nS*G0-!VIq$)Nc5nm!|E@?>u2-n@1vRLLyZj}5YtN*6!M<< zOy_{P-HC#1{p@Ui$P)+Nbv^ubaA{z9SvX}O3d*>xt+Sf5A&)-RCTlMoQ}9tNX|jST7>V|N>t1W;SP>AkFtfb?L}0PGjlXdX>6`A|n#*9ZoBjLmJ$jtu@M)spXu1h{rE9#U!;L)oa@5_TeBcXj z^fkk-zNC7aVMn3fhF>E$+q=IDxcgs??8rHQZZ|*ypLqvbB7{>SCxSMann@2Io=n}X z%7~o075{8;j}WkUZw2na=^Us(yJIb^oqU8Stfefk&;y9*ikI?NNVOE3`o146oRS<3q$C$vtSfgcy06Mm zk)_Va`P@cbR0inFhm2=A(L8m>R5WG%HG6)GMme&eP)F88zXRh%$dk0uo;5|HMO`^i zYN=~#4X&4H4@(1I#*q?I;U!O?zt>4iFf7MPHSBi-3w|qy+!3L8FW_Bnkgx6(dR9bE zAZ@3+{AU|*#x}ZJGe$1FpO<(vA>%e*)?alq(1m3V)-||WuVRXEMzBZZQr@YjO_MtQ z2d;)|Fvt5!t#mK_TjHC`HF=>CHL6Anc{iQyd{L^LKgL=7k&s* z)H(fbXt|(~XD{doockAi!kTMO4Vvudm>UBu>%9+V-| ztoJPohzg2`3IQ>A4V+!Zkd;i*yNFO3g<-aZd5>m~FXB4_{}D7&)q;uqo$o?=8~5kn z?e(p0CS2#&?BT325?wXi$rIiAae#bKNC)WY&E>?jx(0hxblMdOuZbpb^MTNhHBZS3 z9W)GD@6i@Mk^gy0q`*YjnwoSt;_2Q}XUZM_|CGjcZ#aH7h+6 zuFje@W+`u4oh`Hn)1QIsFI6nIkzCW`&Ji8cUfmwo>{zH;hU7eJ*u<`4w_pv z?|X9#P3lX3O7A)0|!NF;)c-Jx5rN(eX-4)W}A(W}#*w={`b> zryX~L#<}~ueW06LNJwM%*y+)4iym1!@Mpnso^AHP4bfV2i;Y+G2t>8cJfMk1_$c29 z@_Iv9FQ$^{%L$Wj$Q&24Rf0?Gc-0nhoIB+z0ZT0JLhxvRoAE@zbNofM}mV|K)1 zh0hkjwTSd_j4zrm;a-m6(DGc)&?Hg&P4((#Wq#lE_`|6Y6$V z{I^zZ)MYL2fH_#G>M=>7VAzgLrOiBiIGsD9{uOVbG9KO*e45KeJQ37a;vtvIT)U6d z)qhV(97GkCe@R?rXYKu@B`FUBBri3&@7lkzgB(GmR2|`Jlpy<8oVMK{O1P=rY5+@x zh6jcFcHA`jifd?#=1M!zZSA=QR%R0u=T0AX73T+oBL89c%dwrbU^X4e+Kjj|)kznO zmmxFe$i?OT55~pyGr{zp`KdqUL!H_aTy-RAiTm?~Y+w7iIq%R_-_Ld411Jtu*WrN_ zhPST|DRS#ts370d3t9Bxs)+JRsTmoKtV3L#s*uY?PMkh%e&#ksocGxyNS1ZO7NkfC z>FW>~wd%X@gunHPkCnbWP-q@y&_E&*S-lj^p<*|9)Inp9@Kv~ISzyQHy$gkPLwkXn z3FHH(lZyjc?F9OX3NJ%{T0vQLE0Psa%8XF#-pDdkdx%-gLV3|grzTwHgkPID!kA~g1wUG!`y@yK$rrfhh{r+~8)nhQX<;aFD3gfX|$x1UeQZwB8joFi=Fae=kK^I(-={*zY ze27cJ7s&EL&=Zg+T!z00jqE_o$r*`}0&I`k>`f3Og|2@jrW>TlcOr+ynOO%XE5ii`bF zVeXa2lZeXp^!g8P{3ULD+tXzOwE%;%8Gq?xGe@;2(z0@NxwH!jd9vzhbzioK9~vvt zTi_R$m=)1L+1%K$+Ri$$>s>`fE%xj}WIUl+J&&~ZTG$^+2vtoy()}(!X#Ce|9g0qx zarl#%rL-Ci2*fY3HqD*q$DT*pKq#C?nMwL`b#whVlk^cbyyQYZl})NBClX1;#PV_p z{ouS}O7P+d7qoc28AxkZOLONtBc7U0Sd|xwt<_qfZI0_!OV!yAVl$K3ggW=k<^w%5 z4tAcoxj{X>mF*ss)n~Yv4R=w>Ygy3DWnX+N=id`=vO&a8wXij>TE?Clg+P;>u)w42 z69%I#b9nu^16TLN6A$e5JLvcEg=Rz(<9Sr@p^jNXdmKl7<7Tt^3ATw%*F|Y{xMV{u zTbKreUmez<4?fi&wyzQ-gI2duVL|wSssz(E>ro zCpnJ`=?r&0os#Sj2}PjsOFDx$YQ-vQPr|7)T%UudqSohG?@uuJ7?KR@yFgL?PJdP} zMqwN#plnSK4lXI}?d>J3h!mf56aZtoK$HFWDyKVrn$dfIy1zbDy7{Wan0uvPD>5~QI+$^Ur@#OT|GV}NcCg2L~ZU0 z+dAoe>&5?7@QbV4;3KGC%S(tYnms znx1aA#6`>rF5DC1VPe^4L?gByDiLiBmaNlun;G)($5RuHMtIRz28Zgw8nAxD1GkKQ zd|R)8IC7$7y75=Uq?i~ny!h@RqSBE(kt(LQqNq?JoiiAwS@_9N^U#ki!TC69E>2b4 zekDwdL2j{^uD%m0YxYr9k>^wJ-k7^&l+pC zk|t?O3s7s)ev>)}eJ&iAnO6d8(S3xsNBR!wYgd?dr3&z<2yNImgKbIJALd-#Mykym zyIQp?95eY!H*L<_w>o_5u)txXl$ga!EAS97>gt&4F1?2`B*?w=`eUj7(vyzj&%#oX zLEL_a^eBV35A+QWEX0fozyw9W@Xl$f5qj3KqO6F|n?4hz%2dq3k z4our|p`j}(oCrFVbbDs0A&kH1p~R-{DVVw3tpq_Nr9U9@y8fY!E;&#ea-D1SzC3j? z;z$|khMTtjeN5lYa%B1G43U|RVh3GHXgPB*1=dV*2(gJ{Ec-$0&?BJ9j-4$Yd9=?@G(0TXgn6TE^aY_1TG?LW zlG!m4Td&^&SM;<44*Y_->(tQLX-}4;L^u;q*f0}>My{zRJx>&FO8508g)Vnm;B9ne zu_@pVIpwhjPB=b<-Pgr*qQlS;;T?(VJau1QNX^XPszXIu_-^9YQ#2RMI8ggrtb5aH zBc(NwKgj9|@1vz4&YF9oXvk1?)hta$#?E#{Y<|Fg!2(LO80}7&RB@QKcG$4E62-Qw zk$*p;G`MjW<-LSN=uwcmNxlFwEU8iKh>*^>V3NuB`9bn9iP-tw*^784AN}O6!r>n0 z3Q0OEu5tTBqKc`K1*}GO_bCJh`>&6%7u!$Tp6VCGhl3^<4pWHwwQ8CUE-4>tWmUQ5 zsJKxUn^h*Z^O%xh+;Siu3=a6UV)D{e-Hmxq6@fLzoJ07Vyw!Z9G;52|)jZw595=Mz z?=lJ)-0ROyK)st-8SDDce^#(bjqkQ%7a2L1mY$|#S}&>3w!>-Z_S@R zy4hLDRx_iyJJ{rEnS|>;slu{mC-Mgs=dEfq0pYQ%8b_@#wx>$4;_zk+v*n-=okhIV zHTH#37-Wl^JOEbB9HgTv!jJu)*fq>_x>Y@TP`*>1?Q#$|dc=oiXuQVd~b)x=oE+WY;W>9?JU~95qSTO))|Nt~qPP zj0Qb?C(fOJz*3fd{gD`}SzV#3i?sw53JY zoL=J(cl$>DFy`nuCleg8&eTa_J-twk7#+btp)k85+f|0cMEdEy3hum+p4k+7kl=zB&zpjKKWm z=MAh&fO^UM5P_rSF}L$KLs*|~cyh+pe)+;M7#Q=AEd!d&w)!@Z(*2{)a;C`AJ~`cR zEx2s5(w7U_0XS}P;EEsa< z)v*7-2T=@V#PpIlLHp5jPLI57B;M% zc)NZJIDc`zZ)@iFk-ZP224T!vMg|gVE5!j z6e|5Xlj}}q59uWC>+Fbhan#pC$?a|9%<=tFjL8=HQK_23E8ka{5^TLjF+eP8J@Ug9 zoVHK>p674QrG^$jVJDZfyVgwL)kBKnZ}%basQeM!b#ry%F3|fW-Y%6P8p`$b2-C72 zDU zt7j%2g%%~Vl^}Os3_>&3-I4?`t#U7SBhpZ|@_m}o+0=Ix3N~DCmD}%Q&|0!OMUw^x z?yG~<4=AK~dvKW2u3(XZiO1{H=lbT}86WiTFPrPqc=PI4PU~=P{fEIurQn4tD&u8z zB%lyf3|@ocZ-I7SU;jk|yhnWi9!31@{i%na_VCj$enx_y5%Fhc_+TYx TgBrH~GtuCz@tM3+j@SPW{jN~e literal 0 HcmV?d00001 diff --git a/lib/core.dart b/lib/core.dart index 78d1a1c7..79943e7c 100644 --- a/lib/core.dart +++ b/lib/core.dart @@ -1,6 +1,5 @@ export 'core/constants.dart'; export 'core/configuration.dart'; -export 'core/data.dart'; export 'core/database.dart'; export 'core/dialogs.dart'; export 'core/extensions.dart'; @@ -9,7 +8,8 @@ export 'core/logger.dart'; export 'core/module_flags.dart'; export 'core/providers.dart'; export 'core/routes.dart'; -export 'core/sorter.dart'; +export 'core/state.dart'; export 'core/theme.dart'; +export 'core/types.dart'; export 'core/ui.dart'; export 'core/uuid.dart'; \ No newline at end of file diff --git a/lib/core/configuration/import.dart b/lib/core/configuration/import.dart index 06d662bb..30346586 100644 --- a/lib/core/configuration/import.dart +++ b/lib/core/configuration/import.dart @@ -49,6 +49,12 @@ class Import { wakeOnLANEnabled: profile['wakeOnLANEnabled'] ?? false, wakeOnLANBroadcastAddress: profile['wakeOnLANBroadcastAddress'] ?? '', wakeOnLANMACAddress: profile['wakeOnLANMACAddress'] ?? '', + //Tautulli + tautulliEnabled: profile['tautulliEnabled'] ?? false, + tautulliHost: profile['tautulliHost'] ?? '', + tautulliKey: profile['tautulliKey'] ?? '', + tautulliStrictTLS: profile['tautulliStrictTLS'] ?? true, + tautulliHeaders: profile['tautulliHeaders'] ?? {}, )); } } @@ -57,9 +63,10 @@ class Import { Box _box = Database.indexersBox; for(Map indexer in data) { _box.add(IndexerHiveObject( - displayName: indexer['displayName'], - host: indexer['host'], - key: indexer['key'], + displayName: indexer['displayName'] ?? '', + host: indexer['host'] ?? '', + key: indexer['key'] ?? '', + headers: indexer['headers'] ?? {}, )); } } diff --git a/lib/core/constants.dart b/lib/core/constants.dart index db6da456..a3c1aa07 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -7,7 +7,8 @@ import 'package:lunasea/modules.dart' show SABnzbdConstants, SearchConstants, SettingsConstants, - WakeOnLANConstants; + WakeOnLANConstants, + TautulliConstants; class Constants { Constants._(); @@ -23,6 +24,7 @@ class Constants { SearchConstants.MODULE_KEY: SearchConstants.MODULE_MAP, SettingsConstants.MODULE_KEY: SettingsConstants.MODULE_MAP, WakeOnLANConstants.MODULE_KEY: WakeOnLANConstants.MODULE_MAP, + TautulliConstants.MODULE_KEY: TautulliConstants.MODULE_MAP, }; //Colors static const PRIMARY_COLOR = 0xFF32323E; @@ -38,6 +40,11 @@ class Constants { Colors.purpleAccent, Colors.blueGrey, ]; + //Text + static const TEXT_EMDASH = '—'; + static const TEXT_BULLET = '•'; + static const TEXT_RARROW = '→'; + static const TEXT_LARROW = '←'; //UI static const UI_ELEVATION = 0.0; static const UI_CARD_MARGIN = EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0); @@ -58,6 +65,7 @@ class Constants { static const NO_SERVICES_ENABLED = '<>'; static const CHECK_LOGS_MESSAGE = 'Please check the logs for more details'; //Extensions + static const BIT_SIZES = ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb']; static const BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; //URLs static const URL_DOCUMENTATION = 'https://docs.lunasea.app'; diff --git a/lib/core/data.dart b/lib/core/data.dart deleted file mode 100644 index acb5aaaf..00000000 --- a/lib/core/data.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'data/api.dart'; -export 'data/future_container.dart'; diff --git a/lib/core/data/future_container.dart b/lib/core/data/future_container.dart deleted file mode 100644 index 140aa2e4..00000000 --- a/lib/core/data/future_container.dart +++ /dev/null @@ -1,55 +0,0 @@ -enum FutureContainerStatus { - UNINITIALIZED, - RUNNING, - COMPLETED, - ERROR, -} - -class FutureContainer { - final Function() callback; - FutureContainerStatus _status = FutureContainerStatus.UNINITIALIZED; - Future _future; - T _data; - - FutureContainer(this.callback); - - void start() { - status = FutureContainerStatus.RUNNING; - _future.then((data) => _complete(data)).catchError((error) => _error(error)); - } - - void close() { - _future = null; - _data = null; - status = FutureContainerStatus.UNINITIALIZED; - } - - T _complete(T data) { - _data = data; - status = FutureContainerStatus.COMPLETED; - return _data; - } - - dynamic _error(dynamic error) { - _data = null; - status = FutureContainerStatus.ERROR; - return Future.error(error); - } - - FutureContainerStatus get status => _status; - set status(FutureContainerStatus status) { - assert(status != null); - _status = status; - callback(); - } - - Future get future => _future; - set future(Future future) { - assert(future != null); - _status = FutureContainerStatus.UNINITIALIZED; - _future = future; - start(); - } - - T get data => _data; -} diff --git a/lib/core/database/adapters/indexer.dart b/lib/core/database/adapters/indexer.dart index f17572cb..0aa6b30d 100644 --- a/lib/core/database/adapters/indexer.dart +++ b/lib/core/database/adapters/indexer.dart @@ -10,6 +10,7 @@ class IndexerHiveObject extends HiveObject { displayName: '', host: '', key: '', + headers: {}, ); } @@ -18,6 +19,7 @@ class IndexerHiveObject extends HiveObject { displayName: obj.displayName, host: obj.host, key: obj.key, + headers: obj.headers, ); } @@ -25,6 +27,7 @@ class IndexerHiveObject extends HiveObject { @required this.displayName, @required this.host, @required this.key, + @required this.headers, }); @override @@ -34,9 +37,10 @@ class IndexerHiveObject extends HiveObject { Map toMap() { return { - "displayName": displayName, - "host": host, - "key": key, + "displayName": displayName ?? '', + "host": host ?? '', + "key": key ?? '', + "headers": headers ?? {}, }; } @@ -46,4 +50,6 @@ class IndexerHiveObject extends HiveObject { String host; @HiveField(2) String key; + @HiveField(3) + Map headers; } diff --git a/lib/core/database/adapters/indexer.g.dart b/lib/core/database/adapters/indexer.g.dart index 05fea58f..5834f769 100644 --- a/lib/core/database/adapters/indexer.g.dart +++ b/lib/core/database/adapters/indexer.g.dart @@ -20,19 +20,22 @@ class IndexerHiveObjectAdapter extends TypeAdapter { displayName: fields[0] as String, host: fields[1] as String, key: fields[2] as String, + headers: (fields[3] as Map)?.cast(), ); } @override void write(BinaryWriter writer, IndexerHiveObject obj) { writer - ..writeByte(3) + ..writeByte(4) ..writeByte(0) ..write(obj.displayName) ..writeByte(1) ..write(obj.host) ..writeByte(2) - ..write(obj.key); + ..write(obj.key) + ..writeByte(3) + ..write(obj.headers); } @override diff --git a/lib/core/database/adapters/lunasea.dart b/lib/core/database/adapters/lunasea.dart index 2aaf5af8..9a593ba9 100644 --- a/lib/core/database/adapters/lunasea.dart +++ b/lib/core/database/adapters/lunasea.dart @@ -16,6 +16,9 @@ enum LunaSeaDatabaseValue { THEME_AMOLED, THEME_AMOLED_BORDER, SELECTED_BROWSER, + DRAWER_EXPAND_AUTOMATION, + DRAWER_EXPAND_CLIENTS, + DRAWER_EXPAND_MONITORING, } extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue { @@ -27,6 +30,9 @@ extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue { case LunaSeaDatabaseValue.THEME_AMOLED_BORDER: return 'LUNASEA_THEME_AMOLED_BORDER'; case LunaSeaDatabaseValue.SELECTED_BROWSER: return 'LUNASEA_SELECTED_BROWSER'; case LunaSeaDatabaseValue.CLIENT_IDENTIFIER: return 'LUNASEA_CLIENT_IDENTIFIER'; + case LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION: return 'LUNASEA_DRAWER_EXPAND_AUTOMATION'; + case LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS: return 'LUNASEA_DRAWER_EXPAND_CLIENTS'; + case LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING: return 'LUNASEA_DRAWER_EXPAND_MONITORING'; } throw Exception('key not found'); } @@ -40,6 +46,9 @@ extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue { case LunaSeaDatabaseValue.THEME_AMOLED_BORDER: return _box.get(this.key, defaultValue: false); case LunaSeaDatabaseValue.ENABLED_SENTRY: return _box.get(this.key, defaultValue: true); case LunaSeaDatabaseValue.CLIENT_IDENTIFIER: return _box.get(this.key, defaultValue: null); + case LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION: return _box.get(this.key, defaultValue: true); + case LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS: return _box.get(this.key, defaultValue: true); + case LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING: return _box.get(this.key, defaultValue: true); } throw Exception('data not found'); } diff --git a/lib/core/database/adapters/profile.dart b/lib/core/database/adapters/profile.dart index da3b582f..44f04cd0 100644 --- a/lib/core/database/adapters/profile.dart +++ b/lib/core/database/adapters/profile.dart @@ -47,6 +47,12 @@ class ProfileHiveObject extends HiveObject { wakeOnLANEnabled: false, wakeOnLANBroadcastAddress: '', wakeOnLANMACAddress: '', + //Tautulli + tautulliEnabled: false, + tautulliHost: '', + tautulliKey: '', + tautulliStrictTLS: true, + tautulliHeaders: {}, ); } @@ -89,6 +95,12 @@ class ProfileHiveObject extends HiveObject { wakeOnLANEnabled: obj.wakeOnLANEnabled, wakeOnLANBroadcastAddress: obj.wakeOnLANBroadcastAddress, wakeOnLANMACAddress: obj.wakeOnLANMACAddress, + //Tautulli + tautulliEnabled: obj.tautulliEnabled, + tautulliHost: obj.tautulliHost, + tautulliKey: obj.tautulliKey, + tautulliStrictTLS: obj.tautulliStrictTLS, + tautulliHeaders: obj.tautulliHeaders, ); } @@ -130,6 +142,12 @@ class ProfileHiveObject extends HiveObject { @required this.wakeOnLANEnabled, @required this.wakeOnLANBroadcastAddress, @required this.wakeOnLANMACAddress, + //Tautulli + @required this.tautulliEnabled, + @required this.tautulliHost, + @required this.tautulliKey, + @required this.tautulliStrictTLS, + @required this.tautulliHeaders, }); @override @@ -177,6 +195,12 @@ class ProfileHiveObject extends HiveObject { "wakeOnLANEnabled": wakeOnLANEnabled, "wakeOnLANBroadcastAddress": wakeOnLANBroadcastAddress, "wakeOnLANMACAddress": wakeOnLANMACAddress, + //Tautulli + "tautulliEnabled": tautulliEnabled, + "tautulliHost": tautulliHost, + "tautulliKey": tautulliKey, + "tautulliStrictTLS": tautulliStrictTLS, + "tautulliHeaders": tautulliHeaders, }; } @@ -303,26 +327,63 @@ class ProfileHiveObject extends HiveObject { 'MACAddress': wakeOnLANMACAddress ?? '', }; + //Tautulli + @HiveField(31) + bool tautulliEnabled; + @HiveField(32) + String tautulliHost; + @HiveField(33) + String tautulliKey; + @HiveField(34) + bool tautulliStrictTLS; + @HiveField(35) + Map tautulliHeaders; + + Map getTautulli() => { + 'enabled': tautulliEnabled ?? false, + 'host': tautulliHost ?? '', + 'key': tautulliKey ?? '', + 'strict_tls': tautulliStrictTLS ?? true, + 'headers': tautulliHeaders ?? {}, + }; + List get enabledModules => [ - if(ModuleFlags.AUTOMATION && ModuleFlags.LIDARR && lidarrEnabled) 'lidarr', - if(ModuleFlags.AUTOMATION && ModuleFlags.RADARR && radarrEnabled) 'radarr', - if(ModuleFlags.AUTOMATION && ModuleFlags.SONARR && sonarrEnabled) 'sonarr', - if(ModuleFlags.CLIENTS && ModuleFlags.NZBGET && nzbgetEnabled) 'nzbget', - if(ModuleFlags.CLIENTS && ModuleFlags.SABNZBD && sabnzbdEnabled) 'sabnzbd', + if(ModuleFlags.AUTOMATION && ModuleFlags.LIDARR && (lidarrEnabled ?? false)) 'lidarr', + if(ModuleFlags.AUTOMATION && ModuleFlags.RADARR && (radarrEnabled ?? false)) 'radarr', + if(ModuleFlags.AUTOMATION && ModuleFlags.SONARR && (sonarrEnabled ?? false)) 'sonarr', + if(ModuleFlags.CLIENTS && ModuleFlags.NZBGET && (nzbgetEnabled ?? false)) 'nzbget', + if(ModuleFlags.CLIENTS && ModuleFlags.SABNZBD && (sabnzbdEnabled ?? false)) 'sabnzbd', + if(ModuleFlags.MONITORING && ModuleFlags.TAUTULLI && (tautulliEnabled ?? false)) 'tautulli', ]; List get enabledAutomationModules => [ - if(ModuleFlags.AUTOMATION && ModuleFlags.LIDARR && lidarrEnabled) 'lidarr', - if(ModuleFlags.AUTOMATION && ModuleFlags.RADARR && radarrEnabled) 'radarr', - if(ModuleFlags.AUTOMATION && ModuleFlags.SONARR && sonarrEnabled) 'sonarr', + if(ModuleFlags.AUTOMATION && ModuleFlags.LIDARR && (lidarrEnabled ?? false)) 'lidarr', + if(ModuleFlags.AUTOMATION && ModuleFlags.RADARR && (radarrEnabled ?? false)) 'radarr', + if(ModuleFlags.AUTOMATION && ModuleFlags.SONARR && (sonarrEnabled ?? false)) 'sonarr', ]; List get enabledClientModules => [ - if(ModuleFlags.CLIENTS && ModuleFlags.NZBGET && nzbgetEnabled) 'nzbget', - if(ModuleFlags.CLIENTS && ModuleFlags.SABNZBD && sabnzbdEnabled) 'sabnzbd', + if(ModuleFlags.CLIENTS && ModuleFlags.NZBGET && (nzbgetEnabled ?? false)) 'nzbget', + if(ModuleFlags.CLIENTS && ModuleFlags.SABNZBD && (sabnzbdEnabled ?? false)) 'sabnzbd', ]; - bool get anyAutomationEnabled => lidarrEnabled || radarrEnabled || sonarrEnabled; - bool get anyClientsEnabled => nzbgetEnabled || sabnzbdEnabled; - bool get anythingEnabled => anyAutomationEnabled || anyClientsEnabled; + List get enabledMonitoringModules => [ + if(ModuleFlags.MONITORING && ModuleFlags.TAUTULLI && (tautulliEnabled ?? false)) 'tautulli', + ]; + + bool get anyAutomationEnabled => enabledAutomationModules.isNotEmpty; + bool get anyClientsEnabled => enabledClientModules.isNotEmpty; + bool get anyMonitoringEnabled => enabledMonitoringModules.isNotEmpty; + bool get anythingEnabled => anyAutomationEnabled || anyClientsEnabled || anyMonitoringEnabled; + + @override + Future save({ + @required BuildContext context, + bool resetAll = false, + bool resetTautulli = false, + }) { + super.save(); + Providers.reset(context); + return null; + } } diff --git a/lib/core/database/adapters/profile.g.dart b/lib/core/database/adapters/profile.g.dart index f62dfd97..2449cc6c 100644 --- a/lib/core/database/adapters/profile.g.dart +++ b/lib/core/database/adapters/profile.g.dart @@ -48,13 +48,18 @@ class ProfileHiveObjectAdapter extends TypeAdapter { wakeOnLANEnabled: fields[23] as bool, wakeOnLANBroadcastAddress: fields[24] as String, wakeOnLANMACAddress: fields[25] as String, + tautulliEnabled: fields[31] as bool, + tautulliHost: fields[32] as String, + tautulliKey: fields[33] as String, + tautulliStrictTLS: fields[34] as bool, + tautulliHeaders: (fields[35] as Map)?.cast(), ); } @override void write(BinaryWriter writer, ProfileHiveObject obj) { writer - ..writeByte(31) + ..writeByte(36) ..writeByte(0) ..write(obj.lidarrEnabled) ..writeByte(1) @@ -116,7 +121,17 @@ class ProfileHiveObjectAdapter extends TypeAdapter { ..writeByte(24) ..write(obj.wakeOnLANBroadcastAddress) ..writeByte(25) - ..write(obj.wakeOnLANMACAddress); + ..write(obj.wakeOnLANMACAddress) + ..writeByte(31) + ..write(obj.tautulliEnabled) + ..writeByte(32) + ..write(obj.tautulliHost) + ..writeByte(33) + ..write(obj.tautulliKey) + ..writeByte(34) + ..write(obj.tautulliStrictTLS) + ..writeByte(35) + ..write(obj.tautulliHeaders); } @override diff --git a/lib/core/extensions.dart b/lib/core/extensions.dart index 6f63f1bd..d9cb977e 100644 --- a/lib/core/extensions.dart +++ b/lib/core/extensions.dart @@ -1,6 +1,7 @@ export 'extensions/integer.dart'; export 'extensions/datetime.dart'; export 'extensions/double.dart'; +export 'extensions/duration.dart'; export 'extensions/in_app_purchases.dart'; export 'extensions/flog.dart'; export 'extensions/string.dart'; diff --git a/lib/core/extensions/duration.dart b/lib/core/extensions/duration.dart new file mode 100644 index 00000000..f6641719 --- /dev/null +++ b/lib/core/extensions/duration.dart @@ -0,0 +1,13 @@ +extension DurationExtension on Duration { + //ignore: non_constant_identifier_names + String lsDuration_timestamp() { + String hours = this.inHours.toString().padLeft(2, '0'); + String minutes = (this.inMinutes%60).toString().padLeft(2, '0'); + String seconds = (this.inSeconds%60).toString().padLeft(2, '0'); + return [ + if(hours != '00') '$hours:', + '$minutes:', + '$seconds', + ].join(); + } +} diff --git a/lib/core/extensions/integer/bytes.dart b/lib/core/extensions/integer/bytes.dart index 106b3a0a..3469b827 100644 --- a/lib/core/extensions/integer/bytes.dart +++ b/lib/core/extensions/integer/bytes.dart @@ -1,22 +1,23 @@ import 'package:lunasea/core/constants.dart'; extension IntegerBytesExtension on int { - String _bytesToString({ int decimals = 2, position = 0 }) { + String _bytesToString({ int decimals = 2, position = 0, bool bytes = true }) { if(this == null || this <= 0) return '${0.toStringAsFixed(decimals)} B'; + int chunk = bytes ? 1024 : 1000; double size = this.toDouble(); - while(size > 1024) { - size /= 1024; + while(size > chunk) { + size /= chunk; position++; } - return '${size.toStringAsFixed(decimals)} ${Constants.BYTE_SIZES[position]}'; + return '${size.toStringAsFixed(decimals)} ${bytes ? Constants.BYTE_SIZES[position] : Constants.BIT_SIZES[position]}'; } // ignore: non_constant_identifier_names - String lsBytes_BytesToString({ int decimals = 2 }) => _bytesToString(decimals: decimals, position: 0); + String lsBytes_BytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 0, bytes: bytes); // ignore: non_constant_identifier_names - String lsBytes_KilobytesToString({ int decimals = 2 }) => _bytesToString(decimals: decimals, position: 1); + String lsBytes_KilobytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 1, bytes: bytes); // ignore: non_constant_identifier_names - String lsBytes_MegabytesToString({ int decimals = 2 }) => _bytesToString(decimals: decimals, position: 2); + String lsBytes_MegabytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 2, bytes: bytes); // ignore: non_constant_identifier_names - String lsBytes_GigabytesToString({ int decimals = 2 }) => _bytesToString(decimals: decimals, position: 3); + String lsBytes_GigabytesToString({ int decimals = 2, bool bytes = true }) => _bytesToString(decimals: decimals, position: 3, bytes: bytes); } diff --git a/lib/core/logger.dart b/lib/core/logger.dart index 03aded35..69b69df8 100644 --- a/lib/core/logger.dart +++ b/lib/core/logger.dart @@ -5,6 +5,7 @@ import 'package:lunasea/core/database.dart'; import 'package:sentry/sentry.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:f_logs/f_logs.dart' show FLog, DataLogType, FormatType, LogsConfig; +export 'package:dio/dio.dart' show DioError; class Logger { Logger._(); diff --git a/lib/core/module_flags.dart b/lib/core/module_flags.dart index ca8b932f..41f7c0cc 100644 --- a/lib/core/module_flags.dart +++ b/lib/core/module_flags.dart @@ -13,6 +13,6 @@ class ModuleFlags { static const SABNZBD = true; static const NZBGET = true; //Monitoring - static const MONITORING = false; + static const MONITORING = true; static const TAUTULLI = true; } diff --git a/lib/core/providers.dart b/lib/core/providers.dart index 007121fa..cb1e7551 100644 --- a/lib/core/providers.dart +++ b/lib/core/providers.dart @@ -5,29 +5,43 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules.dart' show - HomeModel, + HomeState, LidarrModel, NZBGetModel, - RadarrGlobalState, + RadarrState, SABnzbdModel, SearchModel, SettingsModel, - SonarrModel; + SonarrModel, + TautulliState; class Providers { Providers._(); + + static void reset(BuildContext context) { + Provider.of(context, listen: false).reset(); + // General + Provider.of(context, listen: false).reset(); + // Monitoring + Provider.of(context, listen: false).reset(); + } static MultiProvider providers({ @required Widget child }) => MultiProvider( - key: ObjectKey(Database.currentProfile), providers: [ - ChangeNotifierProvider(create: (context) => HomeModel()), - ChangeNotifierProvider(create: (context) => LidarrModel()), - ChangeNotifierProvider(create: (context) => NZBGetModel()), - ChangeNotifierProvider(create: (context) => RadarrGlobalState()), - ChangeNotifierProvider(create: (context) => SABnzbdModel()), - ChangeNotifierProvider(create: (context) => SearchModel()), - ChangeNotifierProvider(create: (context) => SettingsModel()), - ChangeNotifierProvider(create: (context) => SonarrModel()), + ChangeNotifierProvider(create: (_) => LunaSeaState()), + // General + ChangeNotifierProvider(create: (_) => HomeState()), + ChangeNotifierProvider(create: (_) => SearchModel()), + ChangeNotifierProvider(create: (_) => SettingsModel()), + // Automation + ChangeNotifierProvider(create: (_) => SonarrModel()), + ChangeNotifierProvider(create: (_) => LidarrModel()), + ChangeNotifierProvider(create: (_) => RadarrState()), + // Clients + ChangeNotifierProvider(create: (_) => NZBGetModel()), + ChangeNotifierProvider(create: (_) => SABnzbdModel()), + // Monitoring + ChangeNotifierProvider(create: (_) => TautulliState()), ], child: child, ); diff --git a/lib/core/routes.dart b/lib/core/routes.dart index 676fb8b2..18eb2cea 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -7,6 +7,7 @@ import 'package:lunasea/modules/radarr/routes.dart'; import 'package:lunasea/modules/sonarr/routes.dart'; import 'package:lunasea/modules/nzbget/routes.dart'; import 'package:lunasea/modules/sabnzbd/routes.dart'; +import 'package:lunasea/modules/tautulli.dart' show TautulliConstants; class Routes { Routes._(); @@ -21,6 +22,7 @@ class Routes { ..._sonarr, ..._sabnzbd, ..._nzbget, + ...TautulliConstants.MODULE_ROUTES, }; } @@ -52,6 +54,9 @@ class Routes { SettingsModulesNZBGetHeaders.ROUTE_NAME: (context) => SettingsModulesNZBGetHeaders(), SettingsModulesSABnzbd.ROUTE_NAME: (context) => SettingsModulesSABnzbd(), SettingsModulesSABnzbdHeaders.ROUTE_NAME: (context) => SettingsModulesSABnzbdHeaders(), + // Monitoring + SettingsModulesTautulli.ROUTE_NAME: (context) => SettingsModulesTautulli(), + SettingsModulesTautulliHeaders.ROUTE_NAME: (context) => SettingsModulesTautulliHeaders(), // General SettingsModulesHome.ROUTE_NAME: (context) => SettingsModulesHome(), SettingsModulesLunaSea.ROUTE_NAME: (context) => SettingsModulesLunaSea(), diff --git a/lib/core/state.dart b/lib/core/state.dart new file mode 100644 index 00000000..772220da --- /dev/null +++ b/lib/core/state.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; + +class LunaSeaState extends ChangeNotifier { + LunaSeaState() { + reset(initialize: true); + } + + /// Reset the state of LunaSea back to the default + void reset({ bool initialize = false }) { + if(initialize) {} + notifyListeners(); + } +} diff --git a/lib/core/types.dart b/lib/core/types.dart new file mode 100644 index 00000000..af4f984a --- /dev/null +++ b/lib/core/types.dart @@ -0,0 +1,2 @@ +export 'types/api.dart'; +export 'types/sorter.dart'; diff --git a/lib/core/data/api.dart b/lib/core/types/api.dart similarity index 100% rename from lib/core/data/api.dart rename to lib/core/types/api.dart diff --git a/lib/core/sorter.dart b/lib/core/types/sorter.dart similarity index 100% rename from lib/core/sorter.dart rename to lib/core/types/sorter.dart diff --git a/lib/core/ui.dart b/lib/core/ui.dart index 1948c396..65ebc4c7 100644 --- a/lib/core/ui.dart +++ b/lib/core/ui.dart @@ -10,6 +10,7 @@ export 'ui/floating_action_button.dart'; export 'ui/icon.dart'; export 'ui/input_bar.dart'; export 'ui/listview.dart'; +export 'ui/loader.dart'; export 'ui/messages.dart'; export 'ui/navigation.dart'; export 'ui/refresh_indicator.dart'; diff --git a/lib/core/ui/card/card.dart b/lib/core/ui/card/card.dart index 87b7bafd..7e17ed2d 100644 --- a/lib/core/ui/card/card.dart +++ b/lib/core/ui/card/card.dart @@ -6,12 +6,14 @@ class LSCard extends StatelessWidget { final EdgeInsets margin; final bool reducedMargin; final Color color; + final Decoration decoration; LSCard({ @required this.child, this.margin = Constants.UI_CARD_MARGIN, this.reducedMargin = false, this.color, + this.decoration, }); @override @@ -21,7 +23,10 @@ class LSCard extends StatelessWidget { LunaSeaDatabaseValue.THEME_AMOLED_BORDER.key, ]), builder: (context, box, widget) => Card( - child: child, + child: Container( + child: child, + decoration: decoration, + ), elevation: Constants.UI_ELEVATION, margin: reducedMargin ? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0) diff --git a/lib/core/ui/icon/icon_custom.dart b/lib/core/ui/icon/icon_custom.dart index d789ae41..1d1dbee7 100644 --- a/lib/core/ui/icon/icon_custom.dart +++ b/lib/core/ui/icon/icon_custom.dart @@ -35,4 +35,5 @@ class CustomIcons { static const IconData code = IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData modules = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData discord = IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData tautulli = IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/lib/core/ui/loader.dart b/lib/core/ui/loader.dart new file mode 100644 index 00000000..2fab1511 --- /dev/null +++ b/lib/core/ui/loader.dart @@ -0,0 +1,26 @@ +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: [ + SpinKitThreeBounce( + color: color != null ? color : LSColors.accent, + size: size, + ), + ], + ); +} diff --git a/lib/core/ui/messages.dart b/lib/core/ui/messages.dart index de3a63e3..ff67b989 100644 --- a/lib/core/ui/messages.dart +++ b/lib/core/ui/messages.dart @@ -1,5 +1,4 @@ export 'messages/error_message.dart'; export 'messages/generic.dart'; -export 'messages/loading.dart'; export 'messages/not_enabled.dart'; export 'messages/typewriter.dart'; diff --git a/lib/core/ui/messages/loading.dart b/lib/core/ui/messages/loading.dart deleted file mode 100644 index aab69529..00000000 --- a/lib/core/ui/messages/loading.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import './typewriter.dart'; - -class LSLoading extends StatelessWidget { - @override - Widget build(BuildContext context) => LSTypewriterMessage( - text: 'Loading...' - ); -} diff --git a/lib/core/ui/navigation/appbar_dropdown.dart b/lib/core/ui/navigation/appbar_dropdown.dart index ff93f642..87165335 100644 --- a/lib/core/ui/navigation/appbar_dropdown.dart +++ b/lib/core/ui/navigation/appbar_dropdown.dart @@ -3,6 +3,7 @@ import 'package:lunasea/core.dart'; // ignore: non_constant_identifier_names Widget LSAppBarDropdown({ + @required BuildContext context, @required String title, @required List profiles, List actions @@ -27,7 +28,10 @@ Widget LSAppBarDropdown({ ), ], ), - onSelected: (result) => LunaSeaDatabaseValue.ENABLED_PROFILE.put(result), + onSelected: (result) { + LunaSeaDatabaseValue.ENABLED_PROFILE.put(result); + Providers.reset(context); + }, itemBuilder: (context) { return >[for(String profile in profiles) PopupMenuItem( value: profile, diff --git a/lib/core/ui/navigation/drawer.dart b/lib/core/ui/navigation/drawer.dart index 905970ed..6c851b03 100644 --- a/lib/core/ui/navigation/drawer.dart +++ b/lib/core/ui/navigation/drawer.dart @@ -53,7 +53,10 @@ class LSDrawer extends StatelessWidget { ), ], ), - onSelected: (result) => LunaSeaDatabaseValue.ENABLED_PROFILE.put(result), + onSelected: (result) { + LunaSeaDatabaseValue.ENABLED_PROFILE.put(result); + Providers.reset(context); + }, itemBuilder: (context) { return >[for(String profile in (profilesBox as Box).keys) PopupMenuItem( value: profile, @@ -112,7 +115,7 @@ class LSDrawer extends StatelessWidget { fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ), ), - initiallyExpanded: true, + initiallyExpanded: LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION.data, children: List.generate( Database.currentProfileObject.enabledAutomationModules.length, (index) => _buildEntry( @@ -132,7 +135,7 @@ class LSDrawer extends StatelessWidget { fontSize: Constants.UI_FONT_SIZE_SUBTITLE, ), ), - initiallyExpanded: true, + initiallyExpanded: LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS.data, children: List.generate( Database.currentProfileObject.enabledClientModules.length, (index) => _buildEntry( @@ -144,6 +147,26 @@ class LSDrawer extends StatelessWidget { ), ), ), + if(ModuleFlags.MONITORING && profile.anyMonitoringEnabled) ExpansionTile( + leading: Icon(CustomIcons.monitoring), + title: Text( + 'Monitoring', + style: TextStyle( + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + ), + initiallyExpanded: LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING.data, + children: List.generate( + Database.currentProfileObject.enabledMonitoringModules.length, + (index) => _buildEntry( + context: context, + route: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]]['route'], + icon: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]]['icon'], + title: Constants.MODULE_MAP[Database.currentProfileObject.enabledMonitoringModules[index]]['name'], + padLeft: true, + ), + ), + ), ]; } diff --git a/lib/core/ui/snackbar.dart b/lib/core/ui/snackbar.dart index 61b9aa43..1ada285a 100644 --- a/lib/core/ui/snackbar.dart +++ b/lib/core/ui/snackbar.dart @@ -13,8 +13,8 @@ const _LEFT_PADDING = 8.0; // ignore: non_constant_identifier_names Future LSSnackBar({ @required BuildContext context, - @required String message, @required String title, + String message = Constants.CHECK_LOGS_MESSAGE, Duration duration, SNACKBAR_TYPE type = SNACKBAR_TYPE.info, bool showButton = false, diff --git a/lib/modules.dart b/lib/modules.dart index 4c5d5efa..201a899f 100644 --- a/lib/modules.dart +++ b/lib/modules.dart @@ -6,4 +6,5 @@ export 'modules/sabnzbd.dart'; export 'modules/search.dart'; export 'modules/settings.dart'; export 'modules/sonarr.dart'; +export 'modules/tautulli.dart'; export 'modules/wake_on_lan.dart'; diff --git a/lib/modules/home/core.dart b/lib/modules/home/core.dart index 9c3ee1d8..fb66fe12 100644 --- a/lib/modules/home/core.dart +++ b/lib/modules/home/core.dart @@ -1,5 +1,5 @@ export 'core/api.dart'; export 'core/adapters.dart'; export 'core/database.dart'; -export 'core/state_global.dart'; +export 'core/state.dart'; export 'core/dialogs.dart'; diff --git a/lib/modules/home/core/adapters.dart b/lib/modules/home/core/adapters.dart index fca84a3b..37803858 100644 --- a/lib/modules/home/core/adapters.dart +++ b/lib/modules/home/core/adapters.dart @@ -1,2 +1,3 @@ export 'adapters/calendar_starting_day.dart'; export 'adapters/calendar_starting_size.dart'; +export 'adapters/calendar_starting_type.dart'; diff --git a/lib/modules/home/core/adapters/calendar_starting_size.dart b/lib/modules/home/core/adapters/calendar_starting_size.dart index 5e09cbdc..cc2ce3bb 100644 --- a/lib/modules/home/core/adapters/calendar_starting_size.dart +++ b/lib/modules/home/core/adapters/calendar_starting_size.dart @@ -12,8 +12,6 @@ enum CalendarStartingSize { TWO_WEEKS, @HiveField(2) ONE_MONTH, - @HiveField(3) - SCHEDULE, } extension CalendarStartingSizeExtension on CalendarStartingSize { @@ -22,7 +20,6 @@ extension CalendarStartingSizeExtension on CalendarStartingSize { case CalendarStartingSize.TWO_WEEKS: return CalendarFormat.twoWeeks; case CalendarStartingSize.ONE_MONTH: return CalendarFormat.month; case CalendarStartingSize.ONE_WEEK: - case CalendarStartingSize.SCHEDULE: default: return CalendarFormat.week; } } @@ -31,7 +28,6 @@ extension CalendarStartingSizeExtension on CalendarStartingSize { switch(this) { case CalendarStartingSize.TWO_WEEKS: return 'Two Weeks'; case CalendarStartingSize.ONE_MONTH: return 'One Month'; - case CalendarStartingSize.SCHEDULE: return 'Schedule'; case CalendarStartingSize.ONE_WEEK: default: return 'One Week'; } @@ -41,7 +37,6 @@ extension CalendarStartingSizeExtension on CalendarStartingSize { switch(this) { case CalendarStartingSize.TWO_WEEKS: return Icons.photo_size_select_large; case CalendarStartingSize.ONE_MONTH: return Icons.photo_size_select_actual; - case CalendarStartingSize.SCHEDULE: return Icons.calendar_view_day; case CalendarStartingSize.ONE_WEEK: default: return Icons.photo_size_select_small; } diff --git a/lib/modules/home/core/adapters/calendar_starting_size.g.dart b/lib/modules/home/core/adapters/calendar_starting_size.g.dart index d3db066e..2cfe0885 100644 --- a/lib/modules/home/core/adapters/calendar_starting_size.g.dart +++ b/lib/modules/home/core/adapters/calendar_starting_size.g.dart @@ -19,8 +19,6 @@ class CalendarStartingSizeAdapter extends TypeAdapter { return CalendarStartingSize.TWO_WEEKS; case 2: return CalendarStartingSize.ONE_MONTH; - case 3: - return CalendarStartingSize.SCHEDULE; default: return null; } @@ -38,9 +36,6 @@ class CalendarStartingSizeAdapter extends TypeAdapter { case CalendarStartingSize.ONE_MONTH: writer.writeByte(2); break; - case CalendarStartingSize.SCHEDULE: - writer.writeByte(3); - break; } } diff --git a/lib/modules/home/core/adapters/calendar_starting_type.dart b/lib/modules/home/core/adapters/calendar_starting_type.dart new file mode 100644 index 00000000..1946a36d --- /dev/null +++ b/lib/modules/home/core/adapters/calendar_starting_type.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/core/database/database.dart'; + +part 'calendar_starting_type.g.dart'; + +@HiveType(typeId: 15, adapterName: 'CalendarStartingTypeAdapter') +enum CalendarStartingType { + @HiveField(0) + CALENDAR, + @HiveField(1) + SCHEDULE, +} + +extension CalendarStartingTypeExtension on CalendarStartingType { + String get data { + switch(this) { + case CalendarStartingType.SCHEDULE: return 'schedule'; + case CalendarStartingType.CALENDAR: + default: return 'calendar'; + } + } + + String get name { + switch(this) { + case CalendarStartingType.SCHEDULE: return 'Schedule'; + case CalendarStartingType.CALENDAR: + default: return 'Calendar'; + } + } + + IconData get icon { + switch(this) { + case CalendarStartingType.SCHEDULE: return Icons.calendar_view_day; + case CalendarStartingType.CALENDAR: + default: return CustomIcons.calendar; + } + } +} \ No newline at end of file diff --git a/lib/modules/home/core/adapters/calendar_starting_type.g.dart b/lib/modules/home/core/adapters/calendar_starting_type.g.dart new file mode 100644 index 00000000..58056b0c --- /dev/null +++ b/lib/modules/home/core/adapters/calendar_starting_type.g.dart @@ -0,0 +1,46 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'calendar_starting_type.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CalendarStartingTypeAdapter extends TypeAdapter { + @override + final int typeId = 15; + + @override + CalendarStartingType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return CalendarStartingType.CALENDAR; + case 1: + return CalendarStartingType.SCHEDULE; + default: + return null; + } + } + + @override + void write(BinaryWriter writer, CalendarStartingType obj) { + switch (obj) { + case CalendarStartingType.CALENDAR: + writer.writeByte(0); + break; + case CalendarStartingType.SCHEDULE: + writer.writeByte(1); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CalendarStartingTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/modules/home/core/database.dart b/lib/modules/home/core/database.dart index 161fa7dd..ff14d042 100644 --- a/lib/modules/home/core/database.dart +++ b/lib/modules/home/core/database.dart @@ -8,6 +8,7 @@ class HomeDatabase { static void registerAdapters() { Hive.registerAdapter(CalendarStartingDayAdapter()); Hive.registerAdapter(CalendarStartingSizeAdapter()); + Hive.registerAdapter(CalendarStartingTypeAdapter()); } } @@ -15,6 +16,7 @@ enum HomeDatabaseValue { NAVIGATION_INDEX, CALENDAR_STARTING_DAY, CALENDAR_STARTING_SIZE, + CALENDAR_STARTING_TYPE, CALENDAR_ENABLE_LIDARR, CALENDAR_ENABLE_RADARR, CALENDAR_ENABLE_SONARR, @@ -27,6 +29,7 @@ extension HomeDatabaseValueExtension on HomeDatabaseValue { case HomeDatabaseValue.NAVIGATION_INDEX: return 'HOME_NAVIGATION_INDEX'; case HomeDatabaseValue.CALENDAR_STARTING_DAY: return 'HOME_CALENDAR_STARTING_DAY'; case HomeDatabaseValue.CALENDAR_STARTING_SIZE: return 'HOME_CALENDAR_STARTING_SIZE'; + case HomeDatabaseValue.CALENDAR_STARTING_TYPE: return 'HOME_CALENDAR_STARTING_TYPE'; case HomeDatabaseValue.CALENDAR_ENABLE_LIDARR: return 'HOME_CALENDAR_ENABLE_LIDARR'; case HomeDatabaseValue.CALENDAR_ENABLE_RADARR: return 'HOME_CALENDAR_ENABLE_RADARR'; case HomeDatabaseValue.CALENDAR_ENABLE_SONARR: return 'HOME_CALENDAR_ENABLE_SONARR'; @@ -41,6 +44,7 @@ extension HomeDatabaseValueExtension on HomeDatabaseValue { case HomeDatabaseValue.NAVIGATION_INDEX: return _box.get(this.key, defaultValue: 0); case HomeDatabaseValue.CALENDAR_STARTING_DAY: return _box.get(this.key, defaultValue: CalendarStartingDay.MONDAY); case HomeDatabaseValue.CALENDAR_STARTING_SIZE: return _box.get(this.key, defaultValue: CalendarStartingSize.ONE_WEEK); + case HomeDatabaseValue.CALENDAR_STARTING_TYPE: return _box.get(this.key, defaultValue: CalendarStartingType.CALENDAR); case HomeDatabaseValue.CALENDAR_ENABLE_LIDARR: return _box.get(this.key, defaultValue: true); case HomeDatabaseValue.CALENDAR_ENABLE_RADARR: return _box.get(this.key, defaultValue: true); case HomeDatabaseValue.CALENDAR_ENABLE_SONARR: return _box.get(this.key, defaultValue: true); diff --git a/lib/modules/home/core/state.dart b/lib/modules/home/core/state.dart new file mode 100644 index 00000000..bc382f79 --- /dev/null +++ b/lib/modules/home/core/state.dart @@ -0,0 +1,35 @@ +import 'package:flutter/foundation.dart'; +import 'package:lunasea/modules/home.dart'; + +class HomeState extends ChangeNotifier { + HomeState() { + reset(initialize: true); + } + + /// Reset the state of Home back to the default + /// + /// If `initialize` is true, resets everything. + /// If false, the navigation index, etc. are not reset. + void reset({ bool initialize = false }) { + if(initialize) { + _navigationIndex = HomeDatabaseValue.NAVIGATION_INDEX.data; + _calendarStartingType = HomeDatabaseValue.CALENDAR_STARTING_TYPE.data; + } + } + + int _navigationIndex; + int get navigationIndex => _navigationIndex; + set navigationIndex(int navigationIndex) { + assert(navigationIndex != null); + _navigationIndex = navigationIndex; + notifyListeners(); + } + + CalendarStartingType _calendarStartingType; + CalendarStartingType get calendarStartingType => _calendarStartingType; + set calendarStartingType(CalendarStartingType calendarStartingType) { + assert(calendarStartingType != null); + _calendarStartingType = calendarStartingType; + notifyListeners(); + } +} diff --git a/lib/modules/home/core/state_global.dart b/lib/modules/home/core/state_global.dart deleted file mode 100644 index fc3ad155..00000000 --- a/lib/modules/home/core/state_global.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class HomeModel extends ChangeNotifier { - int _navigationIndex = 0; - int get navigationIndex => _navigationIndex; - set navigationIndex(int navigationIndex) { - assert(navigationIndex != null); - _navigationIndex = navigationIndex; - notifyListeners(); - } - - bool _showCalendarSchedule = false; - bool get showCalendarSchedule => _showCalendarSchedule; - set showCalendarSchedule(bool showCalendarSchedule) { - assert(showCalendarSchedule != null); - _showCalendarSchedule = showCalendarSchedule; - notifyListeners(); - } -} \ No newline at end of file diff --git a/lib/modules/home/routes/calendar.dart b/lib/modules/home/routes/calendar.dart index ebb255f4..d8f1bd7f 100644 --- a/lib/modules/home/routes/calendar.dart +++ b/lib/modules/home/routes/calendar.dart @@ -55,7 +55,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/home/routes/home.dart b/lib/modules/home/routes/home.dart index 0d3c04c2..b073125d 100644 --- a/lib/modules/home/routes/home.dart +++ b/lib/modules/home/routes/home.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/home.dart'; +import 'package:tuple/tuple.dart'; class Home extends StatefulWidget { static const ROUTE_NAME = '/'; @@ -15,24 +16,9 @@ class _State extends State { final List _refreshKeys = [GlobalKey()]; String _profileState = Database.currentProfileObject.toString(); - @override - void initState() { - super.initState(); - Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); - } - @override Widget build(BuildContext context) => WillPopScope( - onWillPop: () async { - if(_scaffoldKey.currentState.isDrawerOpen) { - //If the drawer is open, return true to close it - return true; - } else { - //If the drawer isn't open, open the drawer - _scaffoldKey.currentState.openDrawer(); - return false; - } - }, + onWillPop: _willPopScope, child: ValueListenableBuilder( valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]), builder: (context, lunaBox, widget) { @@ -53,21 +39,39 @@ class _State extends State { ), ); + Future _willPopScope() async { + if(_scaffoldKey.currentState.isDrawerOpen) { + return true; + } else { + _scaffoldKey.currentState.openDrawer(); + return false; + } + } + Widget get _drawer => LSDrawer(page: 'home'); Widget get _appBar => LSAppBar( title: Constants.APPLICATION_NAME, - actions: [ - Consumer( - builder: (context, home, child) => Visibility( - visible: home.navigationIndex == 1, - child: LSIconButton( - icon: !home.showCalendarSchedule ? Icons.calendar_view_day : CustomIcons.calendar, - onPressed: () async => Provider.of(context, listen: false).showCalendarSchedule ^= true, + actions: Database.currentProfileObject.anyAutomationEnabled + ? [ + Selector>( + 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(context, listen: false).calendarStartingType = state.item2 == CalendarStartingType.CALENDAR + ? CalendarStartingType.SCHEDULE + : CalendarStartingType.CALENDAR; + }, + ), ), ), - ), - ], + ] + : null, ); Widget get _bottomNavigationBar => HomeNavigationBar(pageController: _pageController); @@ -80,12 +84,12 @@ class _State extends State { List get _tabs => [ HomeQuickAccess(), - Database.currentProfileObject.anythingEnabled + Database.currentProfileObject.anyAutomationEnabled ? HomeCalendar(refreshIndicatorKey: _refreshKeys[0]) : LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false), ]; - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; + void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; void _refreshProfile() { _profileState = Database.currentProfileObject.toString(); diff --git a/lib/modules/home/routes/quick_access.dart b/lib/modules/home/routes/quick_access.dart index dbafb1a0..71764273 100644 --- a/lib/modules/home/routes/quick_access.dart +++ b/lib/modules/home/routes/quick_access.dart @@ -19,7 +19,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { super.build(context); return widget.profile.anythingEnabled || Database.indexersBox.length > 0 ? _body - : _nothing; + : LSNotEnabled(Constants.NO_SERVICES_ENABLED, showButton: false); } Widget get _body { @@ -59,6 +59,4 @@ class _State extends State with AutomaticKeepAliveClientMixin { }, ); } - - Widget get _nothing => LSGenericMessage(text: 'No Modules Enabled'); -} \ No newline at end of file +} diff --git a/lib/modules/home/widgets/calendar.dart b/lib/modules/home/widgets/calendar.dart index bf5dbdd2..ebe57614 100644 --- a/lib/modules/home/widgets/calendar.dart +++ b/lib/modules/home/widgets/calendar.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:intl/intl.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/home.dart'; @@ -47,9 +46,6 @@ class _State extends State with TickerProviderStateMixin { _calendarController = CalendarController(); _animationController = AnimationController( vsync: this, duration: kThemeAnimationDuration); _animationController?.forward(); - SchedulerBinding.instance.scheduleFrameCallback((_) =>{ - Provider.of(context, listen: false).showCalendarSchedule = (HomeDatabaseValue.CALENDAR_STARTING_SIZE.data as CalendarStartingSize) == CalendarStartingSize.SCHEDULE - }); } @override @@ -68,7 +64,7 @@ class _State extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( - body: Provider.of(context).showCalendarSchedule + body: Provider.of(context).calendarStartingType == CalendarStartingType.CALENDAR ? Padding( child: Column( children: [ diff --git a/lib/modules/home/widgets/navigation_bar.dart b/lib/modules/home/widgets/navigation_bar.dart index e26a2aad..c037ad12 100644 --- a/lib/modules/home/widgets/navigation_bar.dart +++ b/lib/modules/home/widgets/navigation_bar.dart @@ -29,12 +29,12 @@ class _State extends State { void initState() { super.initState(); SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = HomeDatabaseValue.NAVIGATION_INDEX.data; + Provider.of(context, listen: false).navigationIndex = HomeDatabaseValue.NAVIGATION_INDEX.data; }); } @override - Widget build(BuildContext context) => Selector( + Widget build(BuildContext context) => Selector( selector: (_, model) => model.navigationIndex, builder: (context, index, _) => LSBottomNavigationBar( index: index, @@ -49,6 +49,6 @@ class _State extends State { index, duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), curve: Curves.easeOutSine, - ).then((_) => Provider.of(context, listen: false).navigationIndex = index); + ).then((_) => Provider.of(context, listen: false).navigationIndex = index); } } \ No newline at end of file diff --git a/lib/modules/lidarr/routes/add_details.dart b/lib/modules/lidarr/routes/add_details.dart index 62cf9ae7..d5df5fb7 100644 --- a/lib/modules/lidarr/routes/add_details.dart +++ b/lib/modules/lidarr/routes/add_details.dart @@ -130,7 +130,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ); diff --git a/lib/modules/lidarr/routes/catalogue.dart b/lib/modules/lidarr/routes/catalogue.dart index c3f2d7ca..6e3b4796 100644 --- a/lib/modules/lidarr/routes/catalogue.dart +++ b/lib/modules/lidarr/routes/catalogue.dart @@ -68,7 +68,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/lidarr/routes/details_album.dart b/lib/modules/lidarr/routes/details_album.dart index 04123fea..6ef68225 100644 --- a/lib/modules/lidarr/routes/details_album.dart +++ b/lib/modules/lidarr/routes/details_album.dart @@ -84,7 +84,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/lidarr/routes/details_artist.dart b/lib/modules/lidarr/routes/details_artist.dart index 7149de9d..bce9e424 100644 --- a/lib/modules/lidarr/routes/details_artist.dart +++ b/lib/modules/lidarr/routes/details_artist.dart @@ -61,7 +61,7 @@ class _State extends State { ? _body : _error ? LSErrorMessage(onTapHandler: () => _fetch()) - : LSLoading() + : LSLoader() : null, ); diff --git a/lib/modules/lidarr/routes/edit_artist.dart b/lib/modules/lidarr/routes/edit_artist.dart index 674397ee..c4deb36c 100644 --- a/lib/modules/lidarr/routes/edit_artist.dart +++ b/lib/modules/lidarr/routes/edit_artist.dart @@ -106,7 +106,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ); diff --git a/lib/modules/lidarr/routes/history.dart b/lib/modules/lidarr/routes/history.dart index 99aadca4..8e784cfa 100644 --- a/lib/modules/lidarr/routes/history.dart +++ b/lib/modules/lidarr/routes/history.dart @@ -65,7 +65,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/lidarr/routes/lidarr.dart b/lib/modules/lidarr/routes/lidarr.dart index 0d016d6e..cc25698b 100644 --- a/lib/modules/lidarr/routes/lidarr.dart +++ b/lib/modules/lidarr/routes/lidarr.dart @@ -80,6 +80,7 @@ class _State extends State { ); Widget get _appBar => LSAppBarDropdown( + context: context, title: 'Lidarr', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).lidarrEnabled) diff --git a/lib/modules/lidarr/routes/missing.dart b/lib/modules/lidarr/routes/missing.dart index babbc776..97d759dd 100644 --- a/lib/modules/lidarr/routes/missing.dart +++ b/lib/modules/lidarr/routes/missing.dart @@ -65,7 +65,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/lidarr/routes/search_results.dart b/lib/modules/lidarr/routes/search_results.dart index 333e0d7f..e1e108b9 100644 --- a/lib/modules/lidarr/routes/search_results.dart +++ b/lib/modules/lidarr/routes/search_results.dart @@ -74,7 +74,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSTypewriterMessage(text: 'Searching...'); + default: return LSLoader(); } }, ), diff --git a/lib/modules/lidarr/widgets/details_album_list.dart b/lib/modules/lidarr/widgets/details_album_list.dart index ff52c86f..bdc7b8a5 100644 --- a/lib/modules/lidarr/widgets/details_album_list.dart +++ b/lib/modules/lidarr/widgets/details_album_list.dart @@ -59,7 +59,7 @@ class _State extends State with AutomaticKeepAliveClient case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/nzbget/routes/history.dart b/lib/modules/nzbget/routes/history.dart index 31ce08c3..af968f70 100644 --- a/lib/modules/nzbget/routes/history.dart +++ b/lib/modules/nzbget/routes/history.dart @@ -62,7 +62,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/nzbget/routes/nzbget.dart b/lib/modules/nzbget/routes/nzbget.dart index 4103c82c..7aeaa286 100644 --- a/lib/modules/nzbget/routes/nzbget.dart +++ b/lib/modules/nzbget/routes/nzbget.dart @@ -71,6 +71,7 @@ class _State extends State { ); Widget get _appBar => LSAppBarDropdown( + context: context, title: 'NZBGet', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).nzbgetEnabled) diff --git a/lib/modules/nzbget/routes/statistics.dart b/lib/modules/nzbget/routes/statistics.dart index 4704ebb0..5fe305ea 100644 --- a/lib/modules/nzbget/routes/statistics.dart +++ b/lib/modules/nzbget/routes/statistics.dart @@ -72,7 +72,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/radarr/core/dialogs.dart b/lib/modules/radarr/core/dialogs.dart index 482ece5d..477a6834 100644 --- a/lib/modules/radarr/core/dialogs.dart +++ b/lib/modules/radarr/core/dialogs.dart @@ -50,12 +50,12 @@ class RadarrDialogs { ], content: [ LSDialog.textContent(text: 'Are you sure you want to remove the movie from Radarr?\n'), - Selector( + Selector( selector: (_, state) => state.removeDeleteFiles, builder: (context, value, text) => CheckboxListTile( title: text, value: value, - onChanged: (selected) => Provider.of(context, listen: false).removeDeleteFiles = selected, + onChanged: (selected) => Provider.of(context, listen: false).removeDeleteFiles = selected, contentPadding: LSDialog.tileContentPadding(), ), child: Text( @@ -66,12 +66,12 @@ class RadarrDialogs { ), ), ), - Selector( + Selector( selector: (_, state) => state.removeAddExclusion, builder: (context, value, text) => CheckboxListTile( title: text, value: value, - onChanged: (selected) => Provider.of(context, listen: false).removeAddExclusion = selected, + onChanged: (selected) => Provider.of(context, listen: false).removeAddExclusion = selected, contentPadding: LSDialog.tileContentPadding(), ), child: Text( diff --git a/lib/modules/radarr/core/state.dart b/lib/modules/radarr/core/state.dart index 8493db5e..94f7ec53 100644 --- a/lib/modules/radarr/core/state.dart +++ b/lib/modules/radarr/core/state.dart @@ -1 +1,119 @@ -export 'state/global.dart'; +import 'package:flutter/foundation.dart'; +import 'package:lunasea/modules/radarr.dart'; + +class RadarrState extends ChangeNotifier { + ///Catalogue Sticky Header Content + + String _searchCatalogueFilter = ''; + String get searchCatalogueFilter => _searchCatalogueFilter; + set searchCatalogueFilter(String searchCatalogueFilter) { + assert(searchCatalogueFilter != null); + _searchCatalogueFilter = searchCatalogueFilter; + notifyListeners(); + } + + + RadarrCatalogueSorting _sortCatalogueType = RadarrCatalogueSorting.alphabetical; + RadarrCatalogueSorting get sortCatalogueType => _sortCatalogueType; + set sortCatalogueType(RadarrCatalogueSorting sortCatalogueType) { + assert(sortCatalogueType != null); + _sortCatalogueType = sortCatalogueType; + notifyListeners(); + } + + bool _sortCatalogueAscending = true; + bool get sortCatalogueAscending => _sortCatalogueAscending; + set sortCatalogueAscending(bool sortCatalogueAscending) { + assert(sortCatalogueAscending != null); + _sortCatalogueAscending = sortCatalogueAscending; + notifyListeners(); + } + + bool _hideUnmonitoredMovies = false; + bool get hideUnmonitoredMovies => _hideUnmonitoredMovies; + set hideUnmonitoredMovies(bool hideUnmonitoredMovies) { + assert(hideUnmonitoredMovies != null); + _hideUnmonitoredMovies = hideUnmonitoredMovies; + notifyListeners(); + } + + ///Releases Sticky Header Content + + String _searchReleasesFilter = ''; + String get searchReleasesFilter => _searchReleasesFilter; + set searchReleasesFilter(String searchReleasesFilter) { + assert(searchReleasesFilter != null); + _searchReleasesFilter = searchReleasesFilter; + notifyListeners(); + } + + RadarrReleasesSorting _sortReleasesType = RadarrReleasesSorting.weight; + RadarrReleasesSorting get sortReleasesType => _sortReleasesType; + set sortReleasesType(RadarrReleasesSorting sortReleasesType) { + assert(sortReleasesType != null); + _sortReleasesType = sortReleasesType; + notifyListeners(); + } + + bool _sortReleasesAscending = true; + bool get sortReleasesAscending => _sortReleasesAscending; + set sortReleasesAscending(bool sortReleasesAscending) { + assert(sortReleasesAscending != null); + _sortReleasesAscending = sortReleasesAscending; + notifyListeners(); + } + + bool _hideRejectedReleases = false; + bool get hideRejectedReleases => _hideRejectedReleases; + set hideRejectedReleases(bool hideRejectedReleases) { + assert(hideRejectedReleases != null); + _hideRejectedReleases = hideRejectedReleases; + notifyListeners(); + } + + ///Add New Movie Content + + String _addSearchQuery = ''; + String get addSearchQuery => _addSearchQuery; + set addSearchQuery(String addSearchQuery) { + assert(addSearchQuery != null); + _addSearchQuery = addSearchQuery; + notifyListeners(); + } + + ///Delete options + + bool _removeDeleteFiles = false; + bool get removeDeleteFiles => _removeDeleteFiles; + set removeDeleteFiles(bool removeDeleteFiles) { + assert(removeDeleteFiles != null); + _removeDeleteFiles = removeDeleteFiles; + notifyListeners(); + } + + bool _removeAddExclusion = false; + bool get removeAddExclusion => _removeAddExclusion; + set removeAddExclusion(bool removeAddExclusion) { + assert(removeAddExclusion != null); + _removeAddExclusion = removeAddExclusion; + notifyListeners(); + } + + ///Navigation Indexes + + int _navigationIndex = 0; + int get navigationIndex => _navigationIndex; + set navigationIndex(int navigationIndex) { + assert(navigationIndex != null); + _navigationIndex = navigationIndex; + notifyListeners(); + } + + int _movieNavigationIndex = 0; + int get movieNavigationIndex => _movieNavigationIndex; + set movieNavigationIndex(int movieNavigationIndex) { + assert(movieNavigationIndex != null); + _movieNavigationIndex = movieNavigationIndex; + notifyListeners(); + } +} diff --git a/lib/modules/radarr/core/state/global.dart b/lib/modules/radarr/core/state/global.dart deleted file mode 100644 index 9ee35ff1..00000000 --- a/lib/modules/radarr/core/state/global.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:lunasea/modules/radarr.dart'; - -class RadarrGlobalState extends ChangeNotifier { - ///Catalogue Sticky Header Content - - String _searchCatalogueFilter = ''; - String get searchCatalogueFilter => _searchCatalogueFilter; - set searchCatalogueFilter(String searchCatalogueFilter) { - assert(searchCatalogueFilter != null); - _searchCatalogueFilter = searchCatalogueFilter; - notifyListeners(); - } - - - RadarrCatalogueSorting _sortCatalogueType = RadarrCatalogueSorting.alphabetical; - RadarrCatalogueSorting get sortCatalogueType => _sortCatalogueType; - set sortCatalogueType(RadarrCatalogueSorting sortCatalogueType) { - assert(sortCatalogueType != null); - _sortCatalogueType = sortCatalogueType; - notifyListeners(); - } - - bool _sortCatalogueAscending = true; - bool get sortCatalogueAscending => _sortCatalogueAscending; - set sortCatalogueAscending(bool sortCatalogueAscending) { - assert(sortCatalogueAscending != null); - _sortCatalogueAscending = sortCatalogueAscending; - notifyListeners(); - } - - bool _hideUnmonitoredMovies = false; - bool get hideUnmonitoredMovies => _hideUnmonitoredMovies; - set hideUnmonitoredMovies(bool hideUnmonitoredMovies) { - assert(hideUnmonitoredMovies != null); - _hideUnmonitoredMovies = hideUnmonitoredMovies; - notifyListeners(); - } - - ///Releases Sticky Header Content - - String _searchReleasesFilter = ''; - String get searchReleasesFilter => _searchReleasesFilter; - set searchReleasesFilter(String searchReleasesFilter) { - assert(searchReleasesFilter != null); - _searchReleasesFilter = searchReleasesFilter; - notifyListeners(); - } - - RadarrReleasesSorting _sortReleasesType = RadarrReleasesSorting.weight; - RadarrReleasesSorting get sortReleasesType => _sortReleasesType; - set sortReleasesType(RadarrReleasesSorting sortReleasesType) { - assert(sortReleasesType != null); - _sortReleasesType = sortReleasesType; - notifyListeners(); - } - - bool _sortReleasesAscending = true; - bool get sortReleasesAscending => _sortReleasesAscending; - set sortReleasesAscending(bool sortReleasesAscending) { - assert(sortReleasesAscending != null); - _sortReleasesAscending = sortReleasesAscending; - notifyListeners(); - } - - bool _hideRejectedReleases = false; - bool get hideRejectedReleases => _hideRejectedReleases; - set hideRejectedReleases(bool hideRejectedReleases) { - assert(hideRejectedReleases != null); - _hideRejectedReleases = hideRejectedReleases; - notifyListeners(); - } - - ///Add New Movie Content - - String _addSearchQuery = ''; - String get addSearchQuery => _addSearchQuery; - set addSearchQuery(String addSearchQuery) { - assert(addSearchQuery != null); - _addSearchQuery = addSearchQuery; - notifyListeners(); - } - - ///Delete options - - bool _removeDeleteFiles = false; - bool get removeDeleteFiles => _removeDeleteFiles; - set removeDeleteFiles(bool removeDeleteFiles) { - assert(removeDeleteFiles != null); - _removeDeleteFiles = removeDeleteFiles; - notifyListeners(); - } - - bool _removeAddExclusion = false; - bool get removeAddExclusion => _removeAddExclusion; - set removeAddExclusion(bool removeAddExclusion) { - assert(removeAddExclusion != null); - _removeAddExclusion = removeAddExclusion; - notifyListeners(); - } - - ///Navigation Indexes - - int _navigationIndex = 0; - int get navigationIndex => _navigationIndex; - set navigationIndex(int navigationIndex) { - assert(navigationIndex != null); - _navigationIndex = navigationIndex; - notifyListeners(); - } - - int _movieNavigationIndex = 0; - int get movieNavigationIndex => _movieNavigationIndex; - set movieNavigationIndex(int movieNavigationIndex) { - assert(movieNavigationIndex != null); - _movieNavigationIndex = movieNavigationIndex; - notifyListeners(); - } -} diff --git a/lib/modules/radarr/routes/add_details.dart b/lib/modules/radarr/routes/add_details.dart index 9e071468..685ddaa3 100644 --- a/lib/modules/radarr/routes/add_details.dart +++ b/lib/modules/radarr/routes/add_details.dart @@ -121,7 +121,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ); diff --git a/lib/modules/radarr/routes/add_search.dart b/lib/modules/radarr/routes/add_search.dart index ab404999..0ba25cb4 100644 --- a/lib/modules/radarr/routes/add_search.dart +++ b/lib/modules/radarr/routes/add_search.dart @@ -30,7 +30,7 @@ class _State extends State { } Future _refresh() async { - final _model = Provider.of(context, listen: false); + final _model = Provider.of(context, listen: false); final _api = RadarrAPI.from(Database.currentProfileObject); setState(() { _future = _api.searchMovies(_model.addSearchQuery); diff --git a/lib/modules/radarr/routes/catalogue.dart b/lib/modules/radarr/routes/catalogue.dart index 14de9c8c..4af5e7ba 100644 --- a/lib/modules/radarr/routes/catalogue.dart +++ b/lib/modules/radarr/routes/catalogue.dart @@ -37,7 +37,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { final _api = RadarrAPI.from(Database.currentProfileObject); if(mounted) setState(() => { _future = _api.getAllMovies() }); //Clear the search filter using a microtask - Future.microtask(() => Provider.of(context, listen: false)?.searchCatalogueFilter = ''); + Future.microtask(() => Provider.of(context, listen: false)?.searchCatalogueFilter = ''); } void _refreshState() => setState(() {}); @@ -68,7 +68,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), @@ -91,7 +91,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { buttonText: 'Refresh', onTapHandler: () => _refresh(), ) - : Consumer( + : Consumer( builder: (context, model, widget) { List _filtered = _sort(model, _filter(model.searchCatalogueFilter)); _filtered = model.hideUnmonitoredMovies ? _hide(_filtered) : _filtered; @@ -128,7 +128,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { : entry.title.toLowerCase().contains(filter.toLowerCase()) ).toList(); - List _sort(RadarrGlobalState model, List data) { + List _sort(RadarrState model, List data) { if(data != null && data.length != 0) return model.sortCatalogueType.sort(data, model.sortCatalogueAscending); return data; } diff --git a/lib/modules/radarr/routes/details_movie.dart b/lib/modules/radarr/routes/details_movie.dart index f88c1fa6..b9726dd7 100644 --- a/lib/modules/radarr/routes/details_movie.dart +++ b/lib/modules/radarr/routes/details_movie.dart @@ -31,7 +31,7 @@ class _State extends State { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) { _arguments = ModalRoute.of(context).settings.arguments; - Provider.of(context, listen: false).movieNavigationIndex = 1; + Provider.of(context, listen: false).movieNavigationIndex = 1; _fetch(); }); } @@ -62,7 +62,7 @@ class _State extends State { ? _body : _error ? LSErrorMessage(onTapHandler: () => _fetch()) - : LSLoading() + : LSLoader() : null, ); @@ -93,7 +93,7 @@ class _State extends State { onPageChanged: _onPageChanged, ); - void _onPageChanged(int index) => Provider.of(context, listen: false).movieNavigationIndex = index; + void _onPageChanged(int index) => Provider.of(context, listen: false).movieNavigationIndex = index; Future _removeCallback() async => Navigator.of(context).pop(['remove_movie']); } diff --git a/lib/modules/radarr/routes/edit_movie.dart b/lib/modules/radarr/routes/edit_movie.dart index 876ff8c8..d6efb532 100644 --- a/lib/modules/radarr/routes/edit_movie.dart +++ b/lib/modules/radarr/routes/edit_movie.dart @@ -89,7 +89,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ); diff --git a/lib/modules/radarr/routes/history.dart b/lib/modules/radarr/routes/history.dart index 3d9ce0f4..be3eb6b1 100644 --- a/lib/modules/radarr/routes/history.dart +++ b/lib/modules/radarr/routes/history.dart @@ -64,7 +64,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/radarr/routes/missing.dart b/lib/modules/radarr/routes/missing.dart index ed3862c4..cea634e8 100644 --- a/lib/modules/radarr/routes/missing.dart +++ b/lib/modules/radarr/routes/missing.dart @@ -65,7 +65,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/radarr/routes/radarr.dart b/lib/modules/radarr/routes/radarr.dart index 2e5f120d..83dd2de5 100644 --- a/lib/modules/radarr/routes/radarr.dart +++ b/lib/modules/radarr/routes/radarr.dart @@ -25,7 +25,7 @@ class _State extends State { @override void initState() { super.initState(); - Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); + Future.microtask(() => Provider.of(context, listen: false).navigationIndex = 0); } @override @@ -85,6 +85,7 @@ class _State extends State { ); Widget get _appBar => LSAppBarDropdown( + context: context, title: 'Radarr', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).radarrEnabled) @@ -106,7 +107,7 @@ class _State extends State { ); Future _enterAddMovie() async { - final _model = Provider.of(context, listen: false); + final _model = Provider.of(context, listen: false); _model.addSearchQuery = ''; final dynamic result = await Navigator.of(context).pushNamed(RadarrAddSearch.ROUTE_NAME); if(result != null) switch(result[0]) { @@ -159,7 +160,7 @@ class _State extends State { } } - void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; + void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; void _refreshProfile() { _api = RadarrAPI.from(Database.currentProfileObject); diff --git a/lib/modules/radarr/routes/search_results.dart b/lib/modules/radarr/routes/search_results.dart index cf3347a8..ef94e3ec 100644 --- a/lib/modules/radarr/routes/search_results.dart +++ b/lib/modules/radarr/routes/search_results.dart @@ -43,7 +43,7 @@ class _State extends State { final _api = RadarrAPI.from(Database.currentProfileObject); setState(() => { _future = _api.getReleases(_arguments.movieID) }); //Clear the search filter using a microtask - Future.microtask(() => Provider.of(context, listen: false)?.searchReleasesFilter = ''); + Future.microtask(() => Provider.of(context, listen: false)?.searchReleasesFilter = ''); } @override @@ -74,7 +74,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSTypewriterMessage(text: 'Searching...'); + default: return LSLoader(); } }, ), @@ -97,7 +97,7 @@ class _State extends State { buttonText: 'Refresh', onTapHandler: () => _refresh(), ) - : Consumer( + : Consumer( builder: (context, model, widget) { List _filtered = _sort(model, _filter(model.searchReleasesFilter)); _filtered = model.hideRejectedReleases ? _hide(_filtered) : _filtered; @@ -129,7 +129,7 @@ class _State extends State { : entry.title.toLowerCase().contains(filter.toLowerCase()) ).toList(); - List _sort(RadarrGlobalState model, List data) { + List _sort(RadarrState model, List data) { if(data != null && data.length != 0) return model.sortReleasesType.sort(data, model.sortReleasesAscending); return data; } diff --git a/lib/modules/radarr/routes/upcoming.dart b/lib/modules/radarr/routes/upcoming.dart index 6330b099..e47a47ea 100644 --- a/lib/modules/radarr/routes/upcoming.dart +++ b/lib/modules/radarr/routes/upcoming.dart @@ -65,7 +65,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/radarr/widgets/add_search_bar.dart b/lib/modules/radarr/widgets/add_search_bar.dart index e1b1333f..d24b66d9 100644 --- a/lib/modules/radarr/widgets/add_search_bar.dart +++ b/lib/modules/radarr/widgets/add_search_bar.dart @@ -19,13 +19,13 @@ class _State extends State { @override void initState() { super.initState(); - final model = Provider.of(context, listen: false); + final model = Provider.of(context, listen: false); _controller.text = model.addSearchQuery; } @override Widget build(BuildContext context) => Expanded( - child: Consumer( + child: Consumer( builder: (context, model, widget) => LSTextInputBar( controller: _controller, autofocus: true, @@ -36,7 +36,7 @@ class _State extends State { ), ); - void _onChange(RadarrGlobalState model, String text, updateController) { + void _onChange(RadarrState model, String text, updateController) { model.addSearchQuery = text; if(updateController) _controller.text = text; } diff --git a/lib/modules/radarr/widgets/catalogue_hide_button.dart b/lib/modules/radarr/widgets/catalogue_hide_button.dart index aa3cca9a..46c64cc4 100644 --- a/lib/modules/radarr/widgets/catalogue_hide_button.dart +++ b/lib/modules/radarr/widgets/catalogue_hide_button.dart @@ -17,7 +17,7 @@ class RadarrCatalogueHideButton extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => LSCard( - child: Consumer( + child: Consumer( builder: (context, model, widget) => InkWell( child: LSIconButton( icon: model.hideUnmonitoredMovies diff --git a/lib/modules/radarr/widgets/catalogue_search_bar.dart b/lib/modules/radarr/widgets/catalogue_search_bar.dart index dd9cacb7..172f1a3e 100644 --- a/lib/modules/radarr/widgets/catalogue_search_bar.dart +++ b/lib/modules/radarr/widgets/catalogue_search_bar.dart @@ -12,7 +12,7 @@ class _State extends State { @override Widget build(BuildContext context) => Expanded( - child: Consumer( + child: Consumer( builder: (context, model, widget) => LSTextInputBar( controller: _textController, labelText: 'Search Movies...', @@ -22,7 +22,7 @@ class _State extends State { ), ); - void _onChanged(RadarrGlobalState model, String text, bool update) { + void _onChanged(RadarrState model, String text, bool update) { model.searchCatalogueFilter = text; if(update) _textController.text = ''; } diff --git a/lib/modules/radarr/widgets/catalogue_sorting_button.dart b/lib/modules/radarr/widgets/catalogue_sorting_button.dart index 988fdfb4..4fa59781 100644 --- a/lib/modules/radarr/widgets/catalogue_sorting_button.dart +++ b/lib/modules/radarr/widgets/catalogue_sorting_button.dart @@ -17,7 +17,7 @@ class RadarrCatalogueSortButton extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => LSCard( - child: Consumer( + child: Consumer( builder: (context, model, widget) => PopupMenuButton( shape: LunaSeaDatabaseValue.THEME_AMOLED.data && LunaSeaDatabaseValue.THEME_AMOLED_BORDER.data ? LSRoundedShapeWithBorder() diff --git a/lib/modules/radarr/widgets/catalogue_tile.dart b/lib/modules/radarr/widgets/catalogue_tile.dart index bca554cc..72271a30 100644 --- a/lib/modules/radarr/widgets/catalogue_tile.dart +++ b/lib/modules/radarr/widgets/catalogue_tile.dart @@ -26,7 +26,7 @@ class _State extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Selector( + Selector( selector: (_, model) => model.sortCatalogueType, builder: (context, type, _) => LSSubtitle( text: widget.data.subtitle(type), @@ -177,7 +177,7 @@ class _State extends State { final _api = RadarrAPI.from(Database.currentProfileObject); List values = await RadarrDialogs.deleteMovie(context); if(values[0]) { - RadarrGlobalState _state = Provider.of(context, listen: false); + RadarrState _state = Provider.of(context, listen: false); await _api.removeMovie( widget.data.movieID, deleteFiles: _state.removeDeleteFiles, diff --git a/lib/modules/radarr/widgets/details_edit_button.dart b/lib/modules/radarr/widgets/details_edit_button.dart index 85b37001..221ede66 100644 --- a/lib/modules/radarr/widgets/details_edit_button.dart +++ b/lib/modules/radarr/widgets/details_edit_button.dart @@ -17,7 +17,7 @@ class RadarrDetailsEditButton extends StatefulWidget { class _State extends State { @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, model, widget) => LSIconButton( icon: Icons.edit, onPressed: () async => _handlePopup(context), @@ -58,7 +58,7 @@ class _State extends State { final _api = RadarrAPI.from(Database.currentProfileObject); List values = await RadarrDialogs.deleteMovie(context); if(values[0]) { - RadarrGlobalState _state = Provider.of(context, listen: false); + RadarrState _state = Provider.of(context, listen: false); await _api.removeMovie( widget.data.movieID, deleteFiles: _state.removeDeleteFiles, diff --git a/lib/modules/radarr/widgets/movie_navigation_bar.dart b/lib/modules/radarr/widgets/movie_navigation_bar.dart index 326136ce..6545b040 100644 --- a/lib/modules/radarr/widgets/movie_navigation_bar.dart +++ b/lib/modules/radarr/widgets/movie_navigation_bar.dart @@ -26,7 +26,7 @@ class _State extends State { ]; @override - Widget build(BuildContext context) => Selector( + Widget build(BuildContext context) => Selector( selector: (_, model) => model.movieNavigationIndex, builder: (context, index, _) => LSBottomNavigationBar( index: index, @@ -41,6 +41,6 @@ class _State extends State { index, duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), curve: Curves.easeOutSine, - ).then((_) => Provider.of(context, listen: false).movieNavigationIndex = index); + ).then((_) => Provider.of(context, listen: false).movieNavigationIndex = index); } } diff --git a/lib/modules/radarr/widgets/navigation_bar.dart b/lib/modules/radarr/widgets/navigation_bar.dart index 80360576..d5f89673 100644 --- a/lib/modules/radarr/widgets/navigation_bar.dart +++ b/lib/modules/radarr/widgets/navigation_bar.dart @@ -33,12 +33,12 @@ class _State extends State { void initState() { super.initState(); SchedulerBinding.instance.scheduleFrameCallback((_) { - Provider.of(context, listen: false).navigationIndex = RadarrDatabaseValue.NAVIGATION_INDEX.data; + Provider.of(context, listen: false).navigationIndex = RadarrDatabaseValue.NAVIGATION_INDEX.data; }); } @override - Widget build(BuildContext context) => Selector( + Widget build(BuildContext context) => Selector( selector: (_, model) => model.navigationIndex, builder: (context, index, _) => LSBottomNavigationBar( index: index, @@ -54,6 +54,6 @@ class _State extends State { duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), curve: Curves.easeOutSine, ); - Provider.of(context, listen: false).navigationIndex = index; + Provider.of(context, listen: false).navigationIndex = index; } } \ No newline at end of file diff --git a/lib/modules/radarr/widgets/releases_hide_button.dart b/lib/modules/radarr/widgets/releases_hide_button.dart index 4728d02d..32e8d94a 100644 --- a/lib/modules/radarr/widgets/releases_hide_button.dart +++ b/lib/modules/radarr/widgets/releases_hide_button.dart @@ -17,7 +17,7 @@ class RadarrReleasesHideButton extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => LSCard( - child: Consumer( + child: Consumer( builder: (context, model, widget) => InkWell( child: LSIconButton( icon: model.hideRejectedReleases diff --git a/lib/modules/radarr/widgets/releases_search_bar.dart b/lib/modules/radarr/widgets/releases_search_bar.dart index aa63d866..32b68606 100644 --- a/lib/modules/radarr/widgets/releases_search_bar.dart +++ b/lib/modules/radarr/widgets/releases_search_bar.dart @@ -24,7 +24,7 @@ class _State extends State { @override Widget build(BuildContext context) => Expanded( - child: Consumer( + child: Consumer( builder: (context, model, widget) => LSTextInputBar( controller: _textController, labelText: 'Search Releases...', @@ -34,7 +34,7 @@ class _State extends State { ), ); - void _onChanged(RadarrGlobalState model, String text, bool update) { + void _onChanged(RadarrState model, String text, bool update) { model.searchReleasesFilter = text; if(update) _textController.text = ''; } diff --git a/lib/modules/radarr/widgets/releases_sorting_button.dart b/lib/modules/radarr/widgets/releases_sorting_button.dart index e1c7ac34..b4c89659 100644 --- a/lib/modules/radarr/widgets/releases_sorting_button.dart +++ b/lib/modules/radarr/widgets/releases_sorting_button.dart @@ -17,7 +17,7 @@ class RadarrReleasesSortButton extends StatefulWidget { class _State extends State { @override Widget build(BuildContext context) => LSCard( - child: Consumer( + child: Consumer( builder: (context, model, widget) => PopupMenuButton( shape: LunaSeaDatabaseValue.THEME_AMOLED.data && LunaSeaDatabaseValue.THEME_AMOLED_BORDER.data ? LSRoundedShapeWithBorder() diff --git a/lib/modules/sabnzbd/routes/history.dart b/lib/modules/sabnzbd/routes/history.dart index 9111709c..4576ae92 100644 --- a/lib/modules/sabnzbd/routes/history.dart +++ b/lib/modules/sabnzbd/routes/history.dart @@ -62,7 +62,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/sabnzbd/routes/sabnzbd.dart b/lib/modules/sabnzbd/routes/sabnzbd.dart index 7eb6d7f7..42ab2927 100644 --- a/lib/modules/sabnzbd/routes/sabnzbd.dart +++ b/lib/modules/sabnzbd/routes/sabnzbd.dart @@ -71,6 +71,7 @@ class _State extends State { ); Widget get _appBar => LSAppBarDropdown( + context: context, title: 'SABnzbd', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).sabnzbdEnabled) diff --git a/lib/modules/sabnzbd/routes/statistics.dart b/lib/modules/sabnzbd/routes/statistics.dart index cd448002..d04fa75f 100644 --- a/lib/modules/sabnzbd/routes/statistics.dart +++ b/lib/modules/sabnzbd/routes/statistics.dart @@ -53,7 +53,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/search/core/dialogs.dart b/lib/modules/search/core/dialogs.dart index 30fd3e9b..92c11931 100644 --- a/lib/modules/search/core/dialogs.dart +++ b/lib/modules/search/core/dialogs.dart @@ -60,7 +60,10 @@ class SearchDialogs { ), ), ), - onSelected: (result) => LunaSeaDatabaseValue.ENABLED_PROFILE.put(result), + onSelected: (result) { + LunaSeaDatabaseValue.ENABLED_PROFILE.put(result); + Providers.reset(context); + }, itemBuilder: (context) { return >[for(String profile in (profilesBox as Box).keys) PopupMenuItem( value: profile, diff --git a/lib/modules/search/routes/categories.dart b/lib/modules/search/routes/categories.dart index 2987d6df..2759b253 100644 --- a/lib/modules/search/routes/categories.dart +++ b/lib/modules/search/routes/categories.dart @@ -58,7 +58,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/search/routes/results.dart b/lib/modules/search/routes/results.dart index 9d08fe39..14e0b533 100644 --- a/lib/modules/search/routes/results.dart +++ b/lib/modules/search/routes/results.dart @@ -64,7 +64,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/settings/core/dialogs.dart b/lib/modules/settings/core/dialogs.dart index 79e70437..c2299ab0 100644 --- a/lib/modules/settings/core/dialogs.dart +++ b/lib/modules/settings/core/dialogs.dart @@ -590,7 +590,7 @@ class SettingsDialogs { await LSDialog.dialog( context: context, - title: 'Starting Day of Week', + title: 'Starting Day', content: List.generate( CalendarStartingDay.values.length, (index) => LSDialog.tile( @@ -612,7 +612,6 @@ class SettingsDialogs { void _setValues(bool flag, CalendarStartingSize startingSize) { _flag = flag; _startingSize = startingSize; - Provider.of(context, listen: false).showCalendarSchedule = _startingSize == CalendarStartingSize.SCHEDULE; Navigator.of(context).pop(); } @@ -633,6 +632,33 @@ class SettingsDialogs { return [_flag, _startingSize]; } + static Future> editCalendarStartingType(BuildContext context) async { + bool _flag = false; + CalendarStartingType _startingType; + + void _setValues(bool flag, CalendarStartingType startingType) { + _flag = flag; + _startingType = startingType; + Navigator.of(context).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Starting Type', + content: List.generate( + CalendarStartingType.values.length, + (index) => LSDialog.tile( + icon: CalendarStartingType.values[index].icon, + iconColor: LSColors.list(index), + text: CalendarStartingType.values[index].name, + onTap: () => _setValues(true, CalendarStartingType.values[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return [_flag, _startingType]; + } + static Future> editBroadcastAddress(BuildContext context, String prefill) async { bool _flag = false; final _formKey = GlobalKey(); diff --git a/lib/modules/settings/routes/modules.dart b/lib/modules/settings/routes/modules.dart index 935f17e1..d94ce15e 100644 --- a/lib/modules/settings/routes/modules.dart +++ b/lib/modules/settings/routes/modules.dart @@ -147,11 +147,11 @@ class _State extends State with AutomaticKeepAliveClientMixin { subtitle: 'Configure and customize monitoring modules', ), if(ModuleFlags.TAUTULLI) LSCardTile( - title: LSTitle(text: 'Tautulli'), - subtitle: LSSubtitle(text: 'Configure Tautulli'), + title: LSTitle(text: Constants.MODULE_MAP['tautulli']['name']), + subtitle: LSSubtitle(text: Constants.MODULE_MAP['tautulli']['settings_desc']), trailing: LSIconButton(icon: Icons.arrow_forward_ios), leading: LSIconButton( - icon: CustomIcons.monitoring, + icon: Constants.MODULE_MAP['tautulli']['icon'], color: LSColors.list(9), ), onTap: () async => Navigator.of(context).pushNamed(SettingsModulesTautulli.ROUTE_NAME), diff --git a/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr.dart b/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr.dart index 3b0af881..27f84691 100644 --- a/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr.dart +++ b/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr.dart @@ -35,7 +35,7 @@ class _State extends State { value: _profile.lidarrEnabled ?? false, onChanged: (value) { _profile.lidarrEnabled = value; - _profile.save(); + _profile.save(context: context); }, ), ), @@ -103,12 +103,12 @@ class _State extends State { onChanged: (value) async { if(value) { _profile.lidarrStrictTLS = value; - _profile.save(); + _profile.save(context: context); } else { List _values = await SettingsDialogs.toggleStrictTLS(context); if(_values[0]) { _profile.lidarrStrictTLS = value; - _profile.save(); + _profile.save(context: context); } } }, @@ -133,7 +133,7 @@ class _State extends State { List _values = await SettingsDialogs.editHost(context, 'Lidarr Host', prefill: _profile.lidarrHost ?? ''); if(_values[0]) { _profile.lidarrHost = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -141,7 +141,7 @@ class _State extends State { List _values = await GlobalDialogs.editText(context, 'Lidarr API Key', prefill: _profile.lidarrKey ?? ''); if(_values[0]) { _profile.lidarrKey = _values[1]; - _profile.save(); + _profile.save(context: context); } } diff --git a/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr_headers.dart b/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr_headers.dart index 67bb2d4c..c604c95b 100644 --- a/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr_headers.dart +++ b/lib/modules/settings/routes/modules/automation_lidarr/automation_lidarr_headers.dart @@ -78,7 +78,7 @@ class _State extends State { String _auth = base64.encode(utf8.encode('${results[1]}:${results[2]}')); _headers.addAll({'Authorization': 'Basic $_auth'}); _profile.lidarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -88,7 +88,7 @@ class _State extends State { Map _headers = Map.from(_profile.getLidarr()['headers']); _headers.addAll({results[1]: results[2]}); _profile.lidarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -98,7 +98,7 @@ class _State extends State { Map _headers = Map.from(_profile.getLidarr()['headers']); _headers.remove(key); _profile.lidarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); LSSnackBar( context: context, message: key, diff --git a/lib/modules/settings/routes/modules/automation_radarr/automation_radarr.dart b/lib/modules/settings/routes/modules/automation_radarr/automation_radarr.dart index a7421a4f..5f01aee9 100644 --- a/lib/modules/settings/routes/modules/automation_radarr/automation_radarr.dart +++ b/lib/modules/settings/routes/modules/automation_radarr/automation_radarr.dart @@ -35,7 +35,7 @@ class _State extends State { value: _profile.radarrEnabled ?? false, onChanged: (value) { _profile.radarrEnabled = value; - _profile.save(); + _profile.save(context: context); }, ), ), @@ -85,12 +85,12 @@ class _State extends State { onChanged: (value) async { if(value) { _profile.radarrStrictTLS = value; - _profile.save(); + _profile.save(context: context); } else { List _values = await SettingsDialogs.toggleStrictTLS(context); if(_values[0]) { _profile.radarrStrictTLS = value; - _profile.save(); + _profile.save(context: context); } } }, @@ -133,7 +133,7 @@ class _State extends State { List _values = await SettingsDialogs.editHost(context, 'Radarr Host', prefill: _profile.radarrHost ?? ''); if(_values[0]) { _profile.radarrHost = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -141,7 +141,7 @@ class _State extends State { List _values = await GlobalDialogs.editText(context, 'Radarr API Key', prefill: _profile.radarrKey ?? ''); if(_values[0]) { _profile.radarrKey = _values[1]; - _profile.save(); + _profile.save(context: context); } } diff --git a/lib/modules/settings/routes/modules/automation_radarr/automation_radarr_headers.dart b/lib/modules/settings/routes/modules/automation_radarr/automation_radarr_headers.dart index b989a3a6..133d8d99 100644 --- a/lib/modules/settings/routes/modules/automation_radarr/automation_radarr_headers.dart +++ b/lib/modules/settings/routes/modules/automation_radarr/automation_radarr_headers.dart @@ -78,7 +78,7 @@ class _State extends State { String _auth = base64.encode(utf8.encode('${results[1]}:${results[2]}')); _headers.addAll({'Authorization': 'Basic $_auth'}); _profile.radarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -88,7 +88,7 @@ class _State extends State { Map _headers = Map.from(_profile.getRadarr()['headers']); _headers.addAll({results[1]: results[2]}); _profile.radarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -98,7 +98,7 @@ class _State extends State { Map _headers = Map.from(_profile.getRadarr()['headers']); _headers.remove(key); _profile.radarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); LSSnackBar( context: context, message: key, diff --git a/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr.dart b/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr.dart index ce8db7a2..5241a908 100644 --- a/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr.dart +++ b/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr.dart @@ -48,7 +48,7 @@ class _State extends State { value: _profile.sonarrEnabled ?? false, onChanged: (value) { _profile.sonarrEnabled = value; - _profile.save(); + _profile.save(context: context); }, ), ), @@ -79,7 +79,7 @@ class _State extends State { // value: _profile.sonarrVersion3 ?? false, // onChanged: (value) async { // _profile.sonarrVersion3 = value; - // _profile.save(); + // _profile.save(context: context); // }, // ), // ), @@ -127,12 +127,12 @@ class _State extends State { onChanged: (value) async { if(value) { _profile.sonarrStrictTLS = value; - _profile.save(); + _profile.save(context: context); } else { List _values = await SettingsDialogs.toggleStrictTLS(context); if(_values[0]) { _profile.sonarrStrictTLS = value; - _profile.save(); + _profile.save(context: context); } } }, @@ -144,7 +144,7 @@ class _State extends State { List _values = await SettingsDialogs.editHost(context, 'Sonarr Host', prefill: _profile.sonarrHost ?? ''); if(_values[0]) { _profile.sonarrHost = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -152,7 +152,7 @@ class _State extends State { List _values = await GlobalDialogs.editText(context, 'Sonarr API Key', prefill: _profile.sonarrKey ?? ''); if(_values[0]) { _profile.sonarrKey = _values[1]; - _profile.save(); + _profile.save(context: context); } } diff --git a/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr_headers.dart b/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr_headers.dart index 51cc59f7..25117c68 100644 --- a/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr_headers.dart +++ b/lib/modules/settings/routes/modules/automation_sonarr/automation_sonarr_headers.dart @@ -78,7 +78,7 @@ class _State extends State { String _auth = base64.encode(utf8.encode('${results[1]}:${results[2]}')); _headers.addAll({'Authorization': 'Basic $_auth'}); _profile.sonarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -88,7 +88,7 @@ class _State extends State { Map _headers = Map.from(_profile.getSonarr()['headers']); _headers.addAll({results[1]: results[2]}); _profile.sonarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -98,7 +98,7 @@ class _State extends State { Map _headers = Map.from(_profile.getSonarr()['headers']); _headers.remove(key); _profile.sonarrHeaders = _headers; - _profile.save(); + _profile.save(context: context); LSSnackBar( context: context, message: key, diff --git a/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget.dart b/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget.dart index 59191b90..219a8fae 100644 --- a/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget.dart +++ b/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget.dart @@ -35,7 +35,7 @@ class _State extends State { value: _profile.nzbgetEnabled ?? false, onChanged: (value) { _profile.nzbgetEnabled = value; - _profile.save(); + _profile.save(context: context); }, ), ), @@ -113,12 +113,12 @@ class _State extends State { onChanged: (value) async { if(value) { _profile.nzbgetStrictTLS = value; - _profile.save(); + _profile.save(context: context); } else { List _values = await SettingsDialogs.toggleStrictTLS(context); if(_values[0]) { _profile.nzbgetStrictTLS = value; - _profile.save(); + _profile.save(context: context); } } }, @@ -132,12 +132,12 @@ class _State extends State { onChanged: (value) async { if(!value) { _profile.nzbgetBasicAuth = value; - _profile.save(); + _profile.save(context: context); } else { List _values = await SettingsDialogs.nzbgetBasicAuthentication(context); if(_values[0]) { _profile.nzbgetBasicAuth = value; - _profile.save(); + _profile.save(context: context); } } }, @@ -162,7 +162,7 @@ class _State extends State { List _values = await SettingsDialogs.editHost(context, 'NZBGet Host', prefill: _profile.nzbgetHost ?? ''); if(_values[0]) { _profile.nzbgetHost = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -170,7 +170,7 @@ class _State extends State { List _values = await GlobalDialogs.editText(context, 'NZBGet Username', prefill: _profile.nzbgetUser ?? ''); if(_values[0]) { _profile.nzbgetUser = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -178,7 +178,7 @@ class _State extends State { List _values = await GlobalDialogs.editText(context, 'NZBGet Password', prefill: _profile.nzbgetPass ?? ''); if(_values[0]) { _profile.nzbgetPass = _values[1]; - _profile.save(); + _profile.save(context: context); } } diff --git a/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget_headers.dart b/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget_headers.dart index 0b72084c..93414997 100644 --- a/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget_headers.dart +++ b/lib/modules/settings/routes/modules/clients_nzbget/clients_nzbget_headers.dart @@ -78,7 +78,7 @@ class _State extends State { String _auth = base64.encode(utf8.encode('${results[1]}:${results[2]}')); _headers.addAll({'Authorization': 'Basic $_auth'}); _profile.nzbgetHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -88,7 +88,7 @@ class _State extends State { Map _headers = Map.from(_profile.getNZBGet()['headers']); _headers.addAll({results[1]: results[2]}); _profile.nzbgetHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -98,7 +98,7 @@ class _State extends State { Map _headers = Map.from(_profile.getNZBGet()['headers']); _headers.remove(key); _profile.nzbgetHeaders = _headers; - _profile.save(); + _profile.save(context: context); LSSnackBar( context: context, message: key, diff --git a/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd.dart b/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd.dart index 77bec434..aba81f77 100644 --- a/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd.dart +++ b/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd.dart @@ -35,7 +35,7 @@ class _State extends State { value: _profile.sabnzbdEnabled ?? false, onChanged: (value) { _profile.sabnzbdEnabled = value; - _profile.save(); + _profile.save(context: context); }, ), ), @@ -103,12 +103,12 @@ class _State extends State { onChanged: (value) async { if(value) { _profile.sabnzbdStrictTLS = value; - _profile.save(); + _profile.save(context: context); } else { List _values = await SettingsDialogs.toggleStrictTLS(context); if(_values[0]) { _profile.sabnzbdStrictTLS = value; - _profile.save(); + _profile.save(context: context); } } }, @@ -133,7 +133,7 @@ class _State extends State { List _values = await SettingsDialogs.editHost(context, 'SABnzbd Host', prefill: _profile.sabnzbdHost ?? ''); if(_values[0]) { _profile.sabnzbdHost = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -141,7 +141,7 @@ class _State extends State { List _values = await GlobalDialogs.editText(context, 'SABnzbd API Key', prefill: _profile.sabnzbdKey ?? ''); if(_values[0]) { _profile.sabnzbdKey = _values[1]; - _profile.save(); + _profile.save(context: context); } } diff --git a/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd_headers.dart b/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd_headers.dart index 4370984c..779d2d2e 100644 --- a/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd_headers.dart +++ b/lib/modules/settings/routes/modules/clients_sabnzbd/clients_sabnzbd_headers.dart @@ -78,7 +78,7 @@ class _State extends State { String _auth = base64.encode(utf8.encode('${results[1]}:${results[2]}')); _headers.addAll({'Authorization': 'Basic $_auth'}); _profile.sabnzbdHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -88,7 +88,7 @@ class _State extends State { Map _headers = Map.from(_profile.getSABnzbd()['headers']); _headers.addAll({results[1]: results[2]}); _profile.sabnzbdHeaders = _headers; - _profile.save(); + _profile.save(context: context); } } @@ -98,7 +98,7 @@ class _State extends State { Map _headers = Map.from(_profile.getSABnzbd()['headers']); _headers.remove(key); _profile.sabnzbdHeaders = _headers; - _profile.save(); + _profile.save(context: context); LSSnackBar( context: context, message: key, diff --git a/lib/modules/settings/routes/modules/general_home.dart b/lib/modules/settings/routes/modules/general_home.dart index 9e2d9272..89bb5559 100644 --- a/lib/modules/settings/routes/modules/general_home.dart +++ b/lib/modules/settings/routes/modules/general_home.dart @@ -44,6 +44,7 @@ class _State extends State { ), SettingsModulesHomeCalendarStartingDateTile(), SettingsModulesHomeCalendarStartingSizeTile(), + SettingsModulesHomeCalendarStartingTypeTile(), if(ModuleFlags.LIDARR) SettingsModulesCalendarEnableLidarrTile(), if(ModuleFlags.RADARR) SettingsModulesCalendarEnableRadarrTile(), if(ModuleFlags.SONARR) SettingsModulesCalendarEnableSonarrTile(), diff --git a/lib/modules/settings/routes/modules/general_lunasea.dart b/lib/modules/settings/routes/modules/general_lunasea.dart index 509c383d..8f0dae0a 100644 --- a/lib/modules/settings/routes/modules/general_lunasea.dart +++ b/lib/modules/settings/routes/modules/general_lunasea.dart @@ -25,6 +25,7 @@ class _State extends State { Widget get _body => LSListView( children: [ ..._appearance, + ..._drawer, if(Platform.isIOS) ..._other, ], ); @@ -45,4 +46,14 @@ class _State extends State { ), SettingsModulesLunaSeaBrowserTile(), ]; + + List get _drawer => [ + LSHeader( + text: 'Drawer', + subtitle: 'Customize the drawer to fit your needs', + ), + SettingsDrawerExpandAutomationTile(), + SettingsDrawerExpandClientsTile(), + SettingsDrawerExpandMonitoringTile(), + ]; } diff --git a/lib/modules/settings/routes/modules/general_wake_on_lan.dart b/lib/modules/settings/routes/modules/general_wake_on_lan.dart index acdca4bf..d145552e 100644 --- a/lib/modules/settings/routes/modules/general_wake_on_lan.dart +++ b/lib/modules/settings/routes/modules/general_wake_on_lan.dart @@ -45,7 +45,7 @@ class _State extends State { value: _profile.wakeOnLANEnabled ?? false, onChanged: (value) { _profile.wakeOnLANEnabled = value; - _profile.save(); + _profile.save(context: context); }, ), ), @@ -75,7 +75,7 @@ class _State extends State { List _values = await SettingsDialogs.editBroadcastAddress(context, _profile.wakeOnLANBroadcastAddress ?? ''); if(_values[0]) { _profile.wakeOnLANBroadcastAddress = _values[1]; - _profile.save(); + _profile.save(context: context); } } @@ -83,7 +83,7 @@ class _State extends State { List _values = await SettingsDialogs.editMACAddress(context, _profile.wakeOnLANMACAddress ?? ''); if(_values[0]) { _profile.wakeOnLANMACAddress = _values[1]; - _profile.save(); + _profile.save(context: context); } } } diff --git a/lib/modules/settings/routes/modules/monitoring_tautulli.dart b/lib/modules/settings/routes/modules/monitoring_tautulli.dart index 4233509a..8a429e03 100644 --- a/lib/modules/settings/routes/modules/monitoring_tautulli.dart +++ b/lib/modules/settings/routes/modules/monitoring_tautulli.dart @@ -1 +1,2 @@ export 'monitoring_tautulli/monitoring_tautulli.dart'; +export 'monitoring_tautulli/monitoring_tautulli_headers.dart'; diff --git a/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli.dart b/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli.dart index 96458275..e4c3defe 100644 --- a/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli.dart +++ b/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart' hide Tautulli; +import 'package:lunasea/modules/settings.dart'; class SettingsModulesTautulli extends StatefulWidget { static const ROUTE_NAME = '/settings/modules/tautulli'; @@ -10,6 +12,7 @@ class SettingsModulesTautulli extends StatefulWidget { class _State extends State { final _scaffoldKey = GlobalKey(); + ProfileHiveObject _profile = Database.currentProfileObject; @override Widget build(BuildContext context) => Scaffold( @@ -20,5 +23,153 @@ class _State extends State { Widget get _appBar => LSAppBar(title: 'Tautulli'); - Widget get _body => LSGenericMessage(text: "Coming Soon!"); + List get _configuration => [ + LSHeader( + text: 'Configuration', + subtitle: 'Mandatory configuration for Tautulli functionality', + ), + LSCardTile( + title: LSTitle(text: 'Enable Tautulli'), + subtitle: null, + trailing: Switch( + value: _profile.tautulliEnabled ?? false, + onChanged: (value) { + _profile.tautulliEnabled = value; + _profile.save(context: context, resetTautulli: true); + }, + ), + ), + LSCardTile( + title: LSTitle(text: 'Host'), + subtitle: LSSubtitle( + text: _profile.tautulliHost == null || _profile.tautulliHost == '' + ? 'Not Set' + : _profile.tautulliHost + ), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: _changeHost, + ), + LSCardTile( + title: LSTitle(text: 'API Key'), + subtitle: LSSubtitle( + text: _profile.tautulliKey == null || _profile.tautulliKey == '' + ? 'Not Set' + : '••••••••••••' + ), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: _changeKey, + ), + LSDivider(), + LSButton( + text: 'Test Connection', + onTap: _testConnection, + ), + ]; + + List get _customization => [ + LSHeader( + text: 'Customization', + subtitle: 'Customize Tautulli to fit your needs', + ), + ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [TautulliDatabaseValue.NAVIGATION_INDEX.key]), + builder: (context, box, _) => LSCardTile( + title: LSTitle(text: 'Default Page'), + subtitle: LSSubtitle( + text: TautulliNavigationBar.titles[TautulliDatabaseValue.NAVIGATION_INDEX.data], + ), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async => _defaultPage(), + ), + ), + ]; + + List get _advanced => [ + LSHeader( + text: 'Advanced', + subtitle: 'Advanced options for users with non-standard networking configurations. Be careful!', + ), + LSCardTile( + title: LSTitle(text: 'Custom Headers'), + subtitle: LSSubtitle(text: 'Add Custom Headers to Requests'), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async => Navigator.of(context).pushNamed(SettingsModulesTautulliHeaders.ROUTE_NAME), + ), + LSCardTile( + title: LSTitle(text: 'Strict SSL/TLS Validation'), + subtitle: LSSubtitle(text: 'For Invalid Certificates'), + trailing: Switch( + value: _profile.tautulliStrictTLS ?? true, + onChanged: (value) async { + if(value) { + _profile.tautulliStrictTLS = value; + _profile.save(context: context, resetTautulli: true); + } else { + List _values = await SettingsDialogs.toggleStrictTLS(context); + if(_values[0]) { + _profile.tautulliStrictTLS = value; + _profile.save(context: context, resetTautulli: true); + } + } + }, + ), + ), + ]; + + Widget get _body => ValueListenableBuilder( + valueListenable: Database.profilesBox.listenable(), + builder: (context, box, widget) { + return LSListView( + children: [ + ..._configuration, + ..._customization, + ..._advanced, + ], + ); + }, + ); + + Future _changeHost() async { + List _values = await SettingsDialogs.editHost(context, 'Tautulli Host', prefill: _profile.tautulliHost ?? ''); + if(_values[0]) { + _profile.tautulliHost = _values[1]; + _profile.save(context: context, resetTautulli: true); + } + } + + Future _changeKey() async { + List _values = await GlobalDialogs.editText(context, 'Tautulli API Key', prefill: _profile.tautulliKey ?? ''); + if(_values[0]) { + _profile.tautulliKey = _values[1]; + _profile.save(context: context, resetTautulli: true); + } + } + + Future _defaultPage() async { + List _values = await TautulliDialogs.defaultPage(context); + if(_values[0]) TautulliDatabaseValue.NAVIGATION_INDEX.put(_values[1]); + } + + Future _testConnection() async { + TautulliState state = Provider.of(context, listen: false); + if(!state.enabled) { + LSSnackBar(context: context, title: 'Tautulli Not Enabled', message: 'Tautulli needs to be enabled', type: SNACKBAR_TYPE.failure); + return; + } + if(_profile.tautulliHost == null || _profile.tautulliHost.isEmpty) { + LSSnackBar(context: context, title: 'Host Required', message: 'Host is required to connect to Tautulli', type: SNACKBAR_TYPE.failure); + return; + } + if(_profile.tautulliKey == null || _profile.tautulliKey.isEmpty) { + LSSnackBar(context: context, title: 'API Key Required', message: 'API key is required to connect to Tautulli', type: SNACKBAR_TYPE.failure); + return; + } + state.api.miscellaneous.arnold() + .then((_) { + LSSnackBar(context: context, title: 'Connected Successfully', message: 'Tautulli is ready to use with LunaSea', type: SNACKBAR_TYPE.success); + }).catchError((error, trace) { + Logger.error('SettingsModulesTautulli', '_testConnection', 'Failed Connection', error, trace, uploadToSentry: false); + LSSnackBar(context: context, title: 'Connection Test Failed', message: Constants.CHECK_LOGS_MESSAGE, type: SNACKBAR_TYPE.failure); + }); + } } diff --git a/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli_headers.dart b/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli_headers.dart new file mode 100644 index 00000000..0ccaf78b --- /dev/null +++ b/lib/modules/settings/routes/modules/monitoring_tautulli/monitoring_tautulli_headers.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsModulesTautulliHeaders extends StatefulWidget { + static const ROUTE_NAME = '/settings/modules/tautulli/headers'; + + @override + State createState() => _State(); +} + +class _State extends State { + ProfileHiveObject _profile = Database.currentProfileObject; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: _appBar, + body: _body, + ); + + Widget get _appBar => LSAppBar(title: 'Custom Headers'); + + Widget get _body => ValueListenableBuilder( + valueListenable: Database.profilesBox.listenable(), + builder: (context, profile, widget) => LSListView( + children: [ + ..._headers, + ], + ), + ); + + List get _headers => [ + LSHeader( + text: 'Custom Headers', + subtitle: 'Define custom headers that will be attached to every request made to the module.', + ), + if((_profile.getTautulli()['headers'] as Map).isEmpty) LSGenericMessage( + text: 'No Custom Headers Added', + ), + ..._list, + LSDivider(), + LSButton( + text: 'Add Header', + onTap: () async => _showAddPrompt(), + ), + ]; + + List get _list { + Map headers = Map.from(_profile.getTautulli()['headers']); + List list = []; + headers.forEach((key, value) => list.add(LSCardTile( + title: LSTitle(text: key.toString()), + subtitle: LSSubtitle(text: value.toString()), + trailing: LSIconButton( + icon: Icons.delete, + color: LSColors.red, + onPressed: () async => _deleteIndexer(key.toString(), value.toString()), + ), + ))); + list.sort((a,b) => (a.title as LSTitle).text.compareTo((b.title as LSTitle).text)); + return list; + } + + Future _showAddPrompt() async { + List results = await SettingsDialogs.addHeader(context); + if(results[0]) switch(results[1]) { + case 1: _showAuthenticationPrompt(); break; + case 100: _showCustomPrompt(); break; + default: break; + } + } + + Future _showAuthenticationPrompt() async { + List results = await SettingsDialogs.addAuthenticationHeader(context); + if(results[0]) { + Map _headers = Map.from(_profile.getTautulli()['headers']); + String _auth = base64.encode(utf8.encode('${results[1]}:${results[2]}')); + _headers.addAll({'Authorization': 'Basic $_auth'}); + _profile.tautulliHeaders = _headers; + _profile.save(context: context, resetTautulli: true); + } + } + + Future _showCustomPrompt() async { + List results = await SettingsDialogs.addCustomHeader(context); + if(results[0]) { + Map _headers = Map.from(_profile.getTautulli()['headers']); + _headers.addAll({results[1]: results[2]}); + _profile.tautulliHeaders = _headers; + _profile.save(context: context, resetTautulli: true); + } + } + + Future _deleteIndexer(String key, String value) async { + List results = await SettingsDialogs.deleteHeader(context); + if(results[0]) { + Map _headers = Map.from(_profile.getTautulli()['headers']); + _headers.remove(key); + _profile.tautulliHeaders = _headers; + _profile.save(context: context, resetTautulli: true); + LSSnackBar( + context: context, + message: key, + title: 'Header Deleted', + type: SNACKBAR_TYPE.success, + ); + } + } +} diff --git a/lib/modules/settings/widgets/general/configuration/restore.dart b/lib/modules/settings/widgets/general/configuration/restore.dart index f38e7717..f875fd63 100644 --- a/lib/modules/settings/widgets/general/configuration/restore.dart +++ b/lib/modules/settings/widgets/general/configuration/restore.dart @@ -36,6 +36,7 @@ class SettingsGeneralConfigurationRestoreTile extends StatelessWidget { message: 'This is not a valid LunaSea v2.x configuration backup', type: SNACKBAR_TYPE.failure, ); + Providers.reset(context); } else { LSSnackBar( context: context, diff --git a/lib/modules/settings/widgets/general/profile/add_tile.dart b/lib/modules/settings/widgets/general/profile/add_tile.dart index 03095675..f51f8d66 100644 --- a/lib/modules/settings/widgets/general/profile/add_tile.dart +++ b/lib/modules/settings/widgets/general/profile/add_tile.dart @@ -22,6 +22,7 @@ class SettingsGeneralAddProfileTile extends StatelessWidget { } else { Database.profilesBox.put(_values[1], ProfileHiveObject.empty()); LunaSeaDatabaseValue.ENABLED_PROFILE.put(_values[1]); + Providers.reset(context); LSSnackBar(context: context, title: 'Profile Added', message: '"${_values[1]}" has been added', type: SNACKBAR_TYPE.success); } } diff --git a/lib/modules/settings/widgets/general/profile/enabled_tile.dart b/lib/modules/settings/widgets/general/profile/enabled_tile.dart index ee9f673f..46b4f382 100644 --- a/lib/modules/settings/widgets/general/profile/enabled_tile.dart +++ b/lib/modules/settings/widgets/general/profile/enabled_tile.dart @@ -22,6 +22,7 @@ class SettingsGeneralEnabledProfileTile extends StatelessWidget { if(values[0]) { if(values[1] != LunaSeaDatabaseValue.ENABLED_PROFILE.data) { LunaSeaDatabaseValue.ENABLED_PROFILE.put(values[1]); + Providers.reset(context); } } } diff --git a/lib/modules/settings/widgets/general/profile/rename_tile.dart b/lib/modules/settings/widgets/general/profile/rename_tile.dart index 625ea53e..a4e27578 100644 --- a/lib/modules/settings/widgets/general/profile/rename_tile.dart +++ b/lib/modules/settings/widgets/general/profile/rename_tile.dart @@ -29,6 +29,7 @@ class SettingsGeneralRenameProfileTile extends StatelessWidget { Database.profilesBox.put(_values[1], ProfileHiveObject.from(obj)); if(LunaSeaDatabaseValue.ENABLED_PROFILE.data == old) LunaSeaDatabaseValue.ENABLED_PROFILE.put(_values[1]); obj.delete(); + Providers.reset(context); LSSnackBar(context: context, title: 'Renamed Profile', message: '"$old" has been renamed to "${_values[1]}"', type: SNACKBAR_TYPE.success); } } diff --git a/lib/modules/settings/widgets/modules/home.dart b/lib/modules/settings/widgets/modules/home.dart index 7abb5566..ed6b81e4 100644 --- a/lib/modules/settings/widgets/modules/home.dart +++ b/lib/modules/settings/widgets/modules/home.dart @@ -1,5 +1,6 @@ export 'home/start_date_tile.dart'; export 'home/start_size_tile.dart'; +export 'home/start_type_tile.dart'; export 'home/enable_lidarr.dart'; export 'home/enable_radarr.dart'; export 'home/enable_sonarr.dart'; diff --git a/lib/modules/settings/widgets/modules/home/start_type_tile.dart b/lib/modules/settings/widgets/modules/home/start_type_tile.dart new file mode 100644 index 00000000..86d75b06 --- /dev/null +++ b/lib/modules/settings/widgets/modules/home/start_type_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/home.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsModulesHomeCalendarStartingTypeTile extends StatelessWidget { + @override + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [HomeDatabaseValue.CALENDAR_STARTING_TYPE.key]), + builder: (context, box, widget) => LSCardTile( + title: LSTitle(text: 'Starting Type'), + subtitle: LSSubtitle(text: (HomeDatabaseValue.CALENDAR_STARTING_TYPE.data as CalendarStartingType).name), + trailing: LSIconButton(icon: Icons.arrow_forward_ios), + onTap: () async => _changeType(context), + ), + ); + + Future _changeType(BuildContext context) async { + List _values = await SettingsDialogs.editCalendarStartingType(context); + if(_values[0]) HomeDatabaseValue.CALENDAR_STARTING_TYPE.put(_values[1]); + } +} diff --git a/lib/modules/settings/widgets/modules/lunasea.dart b/lib/modules/settings/widgets/modules/lunasea.dart index 5fa1d1b1..320659e3 100644 --- a/lib/modules/settings/widgets/modules/lunasea.dart +++ b/lib/modules/settings/widgets/modules/lunasea.dart @@ -1,3 +1,6 @@ -export 'lunasea/amoled_tile.dart'; -export 'lunasea/amoled_border_tile.dart'; -export 'lunasea/browser_tile.dart'; +export 'lunasea/appearance_amoled_tile.dart'; +export 'lunasea/appearance_amoled_border_tile.dart'; +export 'lunasea/drawer_expand_automation_tile.dart'; +export 'lunasea/drawer_expand_clients_tile.dart'; +export 'lunasea/drawer_expand_monitoring_tile.dart'; +export 'lunasea/other_browser_tile.dart'; diff --git a/lib/modules/settings/widgets/modules/lunasea/amoled_border_tile.dart b/lib/modules/settings/widgets/modules/lunasea/appearance_amoled_border_tile.dart similarity index 100% rename from lib/modules/settings/widgets/modules/lunasea/amoled_border_tile.dart rename to lib/modules/settings/widgets/modules/lunasea/appearance_amoled_border_tile.dart diff --git a/lib/modules/settings/widgets/modules/lunasea/amoled_tile.dart b/lib/modules/settings/widgets/modules/lunasea/appearance_amoled_tile.dart similarity index 100% rename from lib/modules/settings/widgets/modules/lunasea/amoled_tile.dart rename to lib/modules/settings/widgets/modules/lunasea/appearance_amoled_tile.dart diff --git a/lib/modules/settings/widgets/modules/lunasea/drawer_expand_automation_tile.dart b/lib/modules/settings/widgets/modules/lunasea/drawer_expand_automation_tile.dart new file mode 100644 index 00000000..f7ae3c57 --- /dev/null +++ b/lib/modules/settings/widgets/modules/lunasea/drawer_expand_automation_tile.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class SettingsDrawerExpandAutomationTile extends StatelessWidget { + @override + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION.key]), + builder: (context, box, widget) => LSCardTile( + title: LSTitle(text: 'Expand Automation'), + subtitle: LSSubtitle(text: 'Expand the Automation Category Initially'), + trailing: Switch( + value: LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION.data, + onChanged: _onChange, + ), + ), + ); + + void _onChange(bool value) => LunaSeaDatabaseValue.DRAWER_EXPAND_AUTOMATION.put(value); +} diff --git a/lib/modules/settings/widgets/modules/lunasea/drawer_expand_clients_tile.dart b/lib/modules/settings/widgets/modules/lunasea/drawer_expand_clients_tile.dart new file mode 100644 index 00000000..1f8b3790 --- /dev/null +++ b/lib/modules/settings/widgets/modules/lunasea/drawer_expand_clients_tile.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class SettingsDrawerExpandClientsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS.key]), + builder: (context, box, widget) => LSCardTile( + title: LSTitle(text: 'Expand Clients'), + subtitle: LSSubtitle(text: 'Expand the Clients Category Initially'), + trailing: Switch( + value: LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS.data, + onChanged: _onChange, + ), + ), + ); + + void _onChange(bool value) => LunaSeaDatabaseValue.DRAWER_EXPAND_CLIENTS.put(value); +} diff --git a/lib/modules/settings/widgets/modules/lunasea/drawer_expand_monitoring_tile.dart b/lib/modules/settings/widgets/modules/lunasea/drawer_expand_monitoring_tile.dart new file mode 100644 index 00000000..5ff7e49d --- /dev/null +++ b/lib/modules/settings/widgets/modules/lunasea/drawer_expand_monitoring_tile.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class SettingsDrawerExpandMonitoringTile extends StatelessWidget { + @override + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING.key]), + builder: (context, box, widget) => LSCardTile( + title: LSTitle(text: 'Expand Monitoring'), + subtitle: LSSubtitle(text: 'Expand the Monitoring Category Initially'), + trailing: Switch( + value: LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING.data, + onChanged: _onChange, + ), + ), + ); + + void _onChange(bool value) => LunaSeaDatabaseValue.DRAWER_EXPAND_MONITORING.put(value); +} diff --git a/lib/modules/settings/widgets/modules/lunasea/browser_tile.dart b/lib/modules/settings/widgets/modules/lunasea/other_browser_tile.dart similarity index 100% rename from lib/modules/settings/widgets/modules/lunasea/browser_tile.dart rename to lib/modules/settings/widgets/modules/lunasea/other_browser_tile.dart diff --git a/lib/modules/sonarr/routes/add_details.dart b/lib/modules/sonarr/routes/add_details.dart index 6d416ab3..d7277414 100644 --- a/lib/modules/sonarr/routes/add_details.dart +++ b/lib/modules/sonarr/routes/add_details.dart @@ -130,7 +130,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ); diff --git a/lib/modules/sonarr/routes/catalogue.dart b/lib/modules/sonarr/routes/catalogue.dart index d1ed9851..a7a451a4 100644 --- a/lib/modules/sonarr/routes/catalogue.dart +++ b/lib/modules/sonarr/routes/catalogue.dart @@ -68,7 +68,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/sonarr/routes/details_season.dart b/lib/modules/sonarr/routes/details_season.dart index e70f6448..23b5c09d 100644 --- a/lib/modules/sonarr/routes/details_season.dart +++ b/lib/modules/sonarr/routes/details_season.dart @@ -82,7 +82,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/sonarr/routes/details_series.dart b/lib/modules/sonarr/routes/details_series.dart index f07f0a5b..6fe0daee 100644 --- a/lib/modules/sonarr/routes/details_series.dart +++ b/lib/modules/sonarr/routes/details_series.dart @@ -62,7 +62,7 @@ class _State extends State { ? _body : _error ? LSErrorMessage(onTapHandler: () => _fetch()) - : LSLoading() + : LSLoader() : null, ); diff --git a/lib/modules/sonarr/routes/edit_series.dart b/lib/modules/sonarr/routes/edit_series.dart index daca7b21..e6ac1933 100644 --- a/lib/modules/sonarr/routes/edit_series.dart +++ b/lib/modules/sonarr/routes/edit_series.dart @@ -87,7 +87,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ); diff --git a/lib/modules/sonarr/routes/history.dart b/lib/modules/sonarr/routes/history.dart index 9744aa53..369c44a0 100644 --- a/lib/modules/sonarr/routes/history.dart +++ b/lib/modules/sonarr/routes/history.dart @@ -65,7 +65,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/sonarr/routes/missing.dart b/lib/modules/sonarr/routes/missing.dart index dc790ad9..f38730c6 100644 --- a/lib/modules/sonarr/routes/missing.dart +++ b/lib/modules/sonarr/routes/missing.dart @@ -65,7 +65,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/sonarr/routes/search_results.dart b/lib/modules/sonarr/routes/search_results.dart index 4d37c862..21adf931 100644 --- a/lib/modules/sonarr/routes/search_results.dart +++ b/lib/modules/sonarr/routes/search_results.dart @@ -74,7 +74,7 @@ class _State extends State { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSTypewriterMessage(text: 'Searching...'); + default: return LSLoader(); } }, ), diff --git a/lib/modules/sonarr/routes/sonarr.dart b/lib/modules/sonarr/routes/sonarr.dart index 51d5f034..cf691cc5 100644 --- a/lib/modules/sonarr/routes/sonarr.dart +++ b/lib/modules/sonarr/routes/sonarr.dart @@ -85,6 +85,7 @@ class _State extends State { ); Widget get _appBar => LSAppBarDropdown( + context: context, title: 'Sonarr', profiles: Database.profilesBox.keys.fold([], (value, element) { if((Database.profilesBox.get(element) as ProfileHiveObject).sonarrEnabled) diff --git a/lib/modules/sonarr/routes/upcoming.dart b/lib/modules/sonarr/routes/upcoming.dart index e4f363e7..7264d187 100644 --- a/lib/modules/sonarr/routes/upcoming.dart +++ b/lib/modules/sonarr/routes/upcoming.dart @@ -63,7 +63,7 @@ class _State extends State with AutomaticKeepAliveClientMixin { case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: - default: return LSLoading(); + default: return LSLoader(); } }, ), diff --git a/lib/modules/tautulli.dart b/lib/modules/tautulli.dart new file mode 100644 index 00000000..405066e5 --- /dev/null +++ b/lib/modules/tautulli.dart @@ -0,0 +1,2 @@ +export 'tautulli/core.dart'; +export 'tautulli/modules.dart'; diff --git a/lib/modules/tautulli/core.dart b/lib/modules/tautulli/core.dart new file mode 100644 index 00000000..b13fff38 --- /dev/null +++ b/lib/modules/tautulli/core.dart @@ -0,0 +1,5 @@ +export 'core/constants.dart'; +export 'core/database.dart'; +export 'core/dialogs.dart'; +export 'core/state.dart'; +export 'core/types.dart'; diff --git a/lib/modules/tautulli/core/constants.dart b/lib/modules/tautulli/core/constants.dart new file mode 100644 index 00000000..a349e592 --- /dev/null +++ b/lib/modules/tautulli/core/constants.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliConstants { + TautulliConstants._(); + + static const String MODULE_KEY = 'tautulli'; + + static const Map MODULE_MAP = { + 'name': 'Tautulli', + 'desc': 'View Plex Activity', + 'settings_desc': 'Configure Tautulli', + 'icon': CustomIcons.tautulli, + 'route': '/tautulli', + 'color': Color(0xFFDBA23A), + }; + + // ignore: non_constant_identifier_names + static final Map MODULE_ROUTES = { + Tautulli.ROUTE_NAME: (context) => Tautulli(), + TautulliActivityRoute.ROUTE_NAME: (context) => TautulliActivityRoute(), + TautulliHistoryRoute.ROUTE_NAME: (context) => TautulliHistoryRoute(), + TautulliUsersRoute.ROUTE_NAME: (context) => TautulliUsersRoute(), + TautulliMoreRoute.ROUTE_NAME: (context) => TautulliMoreRoute(), + TautulliActivityDetailsRoute.ROUTE_NAME: (context) => TautulliActivityDetailsRoute(), + TautulliLogsRoute.ROUTE_NAME: (context) => TautulliLogsRoute(), + }; +} diff --git a/lib/modules/tautulli/core/database.dart b/lib/modules/tautulli/core/database.dart new file mode 100644 index 00000000..c7c65c03 --- /dev/null +++ b/lib/modules/tautulli/core/database.dart @@ -0,0 +1,33 @@ +//import 'package:hive/hive.dart'; +import 'package:lunasea/core.dart'; + +class TautulliDatabase { + TautulliDatabase._(); + + static void registerAdapters() {} +} + +enum TautulliDatabaseValue { + NAVIGATION_INDEX, + REFRESH_RATE, +} + +extension TautulliDatabaseValueExtension on TautulliDatabaseValue { + String get key { + switch(this) { + case TautulliDatabaseValue.NAVIGATION_INDEX: return 'TAUTULLI_NAVIGATION_INDEX'; + case TautulliDatabaseValue.REFRESH_RATE: return 'TAUTULLI_REFRESH_RATE'; + } + throw Exception('key not found'); + } + + dynamic get data { + final _box = Database.lunaSeaBox; + switch(this) { + case TautulliDatabaseValue.NAVIGATION_INDEX: return _box.get(this.key, defaultValue: 0); + case TautulliDatabaseValue.REFRESH_RATE: return _box.get(this.key, defaultValue: 10); + } + } + + void put(dynamic value) => Database.lunaSeaBox.put(this.key, value); +} diff --git a/lib/modules/tautulli/core/dialogs.dart b/lib/modules/tautulli/core/dialogs.dart new file mode 100644 index 00000000..827e8d0f --- /dev/null +++ b/lib/modules/tautulli/core/dialogs.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliDialogs { + static Future> globalSettings(BuildContext context) async { + bool _flag = false; + TautulliGlobalSettingsType _value; + + void _setValues(bool flag, TautulliGlobalSettingsType value) { + _flag = flag; + _value = value; + Navigator.of(context).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Tautulli Settings', + content: List.generate( + TautulliGlobalSettingsType.values.length, + (index) => LSDialog.tile( + text: TautulliGlobalSettingsType.values[index].name, + icon: TautulliGlobalSettingsType.values[index].icon, + iconColor: LSColors.list(index), + onTap: () => _setValues(true, TautulliGlobalSettingsType.values[index]), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + return [_flag, _value]; + } + + static Future> defaultPage(BuildContext context) async { + bool _flag = false; + int _index = 0; + + void _setValues(bool flag, int index) { + _flag = flag; + _index = index; + Navigator.of(context).pop(); + } + + await LSDialog.dialog( + context: context, + title: 'Default Page', + content: List.generate( + TautulliNavigationBar.titles.length, + (index) => LSDialog.tile( + text: TautulliNavigationBar.titles[index], + icon: TautulliNavigationBar.icons[index], + iconColor: LSColors.list(index), + onTap: () => _setValues(true, index), + ), + ), + contentPadding: LSDialog.listDialogContentPadding(), + ); + + return [_flag, _index]; + } + + static Future> terminateSession(BuildContext context) async { + bool _flag = false; + GlobalKey _formKey = GlobalKey(); + TextEditingController _textController = TextEditingController(); + + void _setValues(bool flag) { + if(_formKey.currentState.validate()) { + _flag = flag; + Navigator.of(context).pop(); + } + } + + await LSDialog.dialog( + context: context, + title: 'Terminate Session', + buttons: [ + LSDialog.button( + text: 'Terminate', + textColor: LSColors.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + LSDialog.textContent(text: 'Do you want to terminate this session?\n'), + LSDialog.textContent(text: 'You can optionally attach a termination message below.'), + Form( + key: _formKey, + child: LSDialog.textFormInput( + controller: _textController, + title: 'Termination Message', + onSubmitted: (_) => _setValues(true), + validator: (_) => null, + ), + ), + ], + contentPadding: LSDialog.inputTextDialogContentPadding(), + ); + + return [_flag, _textController.text]; + } +} \ No newline at end of file diff --git a/lib/modules/tautulli/core/state.dart b/lib/modules/tautulli/core/state.dart new file mode 100644 index 00000000..c4a60319 --- /dev/null +++ b/lib/modules/tautulli/core/state.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart' hide Tautulli; +import 'package:tautulli/tautulli.dart'; + +class TautulliState extends ChangeNotifier { + TautulliState() { + reset(initialize: true); + } + + /// Reset the state of Tautulli back to the default + /// + /// If `initialize` is true, resets everything, else it resets the profile + data. + /// If false, the navigation index, etc. are not reset. + void reset({ bool initialize = false }) { + resetProfile(); + resetActivity(); + resetUsers(); + if(initialize) { + _navigationIndex = TautulliDatabaseValue.NAVIGATION_INDEX.data; + } + notifyListeners(); + } + + /********** + * PROFILE * + **********/ + + /// API handler instance + Tautulli _api; + Tautulli get api => _api; + + /// Is the API enabled? + bool _enabled; + bool get enabled => _enabled; + + /// Tautulli host + String _host; + String get host => _host; + + /// Tautulli API key + String _apiKey; + String get apiKey => _apiKey; + + /// Is strict TLS enabled? + bool _strictTLS; + bool get strictTLS => _strictTLS; + + /// Headers to attach to all requests + Map _headers; + Map get headers => _headers; + + /// Reset the profile data, reinitializes API instance + void resetProfile() { + ProfileHiveObject _profile = Database.currentProfileObject; + // Copy profile into state + _enabled = _profile.tautulliEnabled ?? false; + _host = _profile.tautulliHost ?? ''; + _apiKey = _profile.tautulliKey ?? ''; + _strictTLS = _profile.tautulliStrictTLS ?? true; + _headers = _profile.tautulliHeaders ?? {}; + // Create the API instance if Tautulli is enabled + _api = _enabled + ? Tautulli( + host: _host, + apiKey: _apiKey, + strictTLS: _strictTLS, + headers: Map.from(_headers), + ) + : null; + } + + /************* + * NAVIGATION * + *************/ + + /// Index for the main page navigation bar + int _navigationIndex; + int get navigationIndex => _navigationIndex; + set navigationIndex(int navigationIndex) { + assert(navigationIndex != null); + _navigationIndex = navigationIndex; + notifyListeners(); + } + + /*********** + * ACTIVITY * + ************/ + + /// Timer to handle refreshing activity data + Timer _getActivityTimer; + + /// Create the periodic timer to handle refreshing activity data + void createActivityTimer() => _getActivityTimer = Timer.periodic( + Duration(seconds: TautulliDatabaseValue.REFRESH_RATE.data), + (_) => activity = _api.activity.getActivity(), + ); + + /// Cancel the periodic timer + void cancelActivityTimer() => _getActivityTimer?.cancel(); + + /// Storing activity data + Future _activity; + Future get activity => _activity; + set activity(Future activity) { + assert(activity != null); + _activity = activity; + notifyListeners(); + } + + /// Reset the activity by: + /// - Cancelling the timer + /// - Recreating the timer + /// - Setting the initial state of the future to an instance of the API call + void resetActivity() { + cancelActivityTimer(); + if(_api != null) { + _activity = _api.activity.getActivity(); + createActivityTimer(); + } + notifyListeners(); + } + + /******** + * USERS * + ********/ + + /// Storing the user data + Future _users; + Future get users => _users; + set users(Future users) { + assert(users != null); + _users = users; + notifyListeners(); + } + + /// Reset the users by: + /// - Setting the intial state of the future to an instance of the API call + void resetUsers() { + if(_api != null) { + _users = _api.users.getUsersTable( + length: 250, + orderDirection: TautulliOrderDirection.ASCENDING, + orderColumn: TautulliUsersOrderColumn.FRIENDLY_NAME, + ); + } + notifyListeners(); + } + + /********* + * IMAGES * + *********/ + + /// Get the direct URL to an image via `pms_image_proxy` using a rating key. + String getImageURLFromRatingKey(int ratingKey) => host.endsWith('/') + ? '${host}api/v2?apikey=$apiKey&cmd=pms_image_proxy&rating_key=$ratingKey' + : '$host/api/v2?apikey=$apiKey&cmd=pms_image_proxy&rating_key=$ratingKey'; + + /// Get the direct URL to an image via `pms_image_proxy` using an image path. + String getImageURLFromPath(String path) => host.endsWith('/') + ? '${host}api/v2?apikey=$apiKey&cmd=pms_image_proxy&img=$path' + : '$host/api/v2?apikey=$apiKey&cmd=pms_image_proxy&img=$path'; +} diff --git a/lib/modules/tautulli/core/types.dart b/lib/modules/tautulli/core/types.dart new file mode 100644 index 00000000..c346e6dd --- /dev/null +++ b/lib/modules/tautulli/core/types.dart @@ -0,0 +1 @@ +export 'types/global_settings.dart'; diff --git a/lib/modules/tautulli/core/types/global_settings.dart b/lib/modules/tautulli/core/types/global_settings.dart new file mode 100644 index 00000000..8a7f3a87 --- /dev/null +++ b/lib/modules/tautulli/core/types/global_settings.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +enum TautulliGlobalSettingsType { + WEB_GUI, + BACKUP_CONFIG, + BACKUP_DB, + DELETE_CACHE, + DELETE_IMAGE_CACHE, +} + +extension TautulliGlobalSettingsTypeExtension on TautulliGlobalSettingsType { + IconData get icon { + switch(this) { + case TautulliGlobalSettingsType.WEB_GUI: return Icons.language; + case TautulliGlobalSettingsType.BACKUP_CONFIG: return Icons.note; + case TautulliGlobalSettingsType.BACKUP_DB: return Icons.save; + case TautulliGlobalSettingsType.DELETE_CACHE: return Icons.cached; + case TautulliGlobalSettingsType.DELETE_IMAGE_CACHE: return Icons.image; + } + throw Exception('Invalid TautulliGlobalSettingsType'); + } + + String get name { + switch(this) { + case TautulliGlobalSettingsType.BACKUP_CONFIG: return 'Backup Configuration'; + case TautulliGlobalSettingsType.BACKUP_DB: return 'Backup Database'; + case TautulliGlobalSettingsType.WEB_GUI: return 'View Web GUI'; + case TautulliGlobalSettingsType.DELETE_CACHE: return 'Delete Cache'; + case TautulliGlobalSettingsType.DELETE_IMAGE_CACHE: return 'Delete Image Cache'; + } + throw Exception('Invalid TautulliGlobalSettingsType'); + } +} diff --git a/lib/modules/tautulli/modules.dart b/lib/modules/tautulli/modules.dart new file mode 100644 index 00000000..e662e1a1 --- /dev/null +++ b/lib/modules/tautulli/modules.dart @@ -0,0 +1,7 @@ +export 'modules/activity.dart'; +export 'modules/activity_details.dart'; +export 'modules/history.dart'; +export 'modules/logs.dart'; +export 'modules/more.dart'; +export 'modules/tautulli.dart'; +export 'modules/users.dart'; diff --git a/lib/modules/tautulli/modules/activity.dart b/lib/modules/tautulli/modules/activity.dart new file mode 100644 index 00000000..92f7349f --- /dev/null +++ b/lib/modules/tautulli/modules/activity.dart @@ -0,0 +1,2 @@ +export 'activity/route.dart'; +export 'activity/widgets.dart'; diff --git a/lib/modules/tautulli/modules/activity/route.dart b/lib/modules/tautulli/modules/activity/route.dart new file mode 100644 index 00000000..6dd40aba --- /dev/null +++ b/lib/modules/tautulli/modules/activity/route.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliActivityRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/activity'; + + TautulliActivityRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Future _refresh() async { + TautulliState _state = Provider.of(context, listen: false); + _state.resetActivity(); + await _state.activity; + } + + Widget get _body => LSRefreshIndicator( + refreshKey: _refreshKey, + onRefresh: _refresh, + child: Selector>( + selector: (_, state) => state.activity, + builder: (context, activity, _) => FutureBuilder( + future: activity, + builder: (context, snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + Logger.error( + 'TautulliActivityRoute', + '_body', + 'Unable to fetch Tautulli activity', + snapshot.error, + null, + uploadToSentry: !(snapshot.error is DioError), + ); + } + return LSErrorMessage(onTapHandler: () async => _refresh()); + } + if(snapshot.hasData) return (snapshot.data as TautulliActivity).streamCount == 0 + ? _noActivity() + : _list((snapshot.data as TautulliActivity)); + return LSLoader(); + }, + ), + ), + ); + + Widget _list(TautulliActivity activity) => LSListView( + children: [ + TautulliActivityStatus(activity: activity), + ...List.generate( + activity.sessions.length, + (index) => TautulliActivityTile(session: activity.sessions[index]), + ), + ], + ); + + Widget _noActivity() => LSGenericMessage( + text: 'No Active Streams', + showButton: true, + buttonText: 'Refresh', + onTapHandler: () async => _refresh(), + ); +} \ No newline at end of file diff --git a/lib/modules/tautulli/modules/activity/widgets.dart b/lib/modules/tautulli/modules/activity/widgets.dart new file mode 100644 index 00000000..cb511b50 --- /dev/null +++ b/lib/modules/tautulli/modules/activity/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/activity_status.dart'; +export 'widgets/activity_tile.dart'; diff --git a/lib/modules/tautulli/modules/activity/widgets/activity_status.dart b/lib/modules/tautulli/modules/activity/widgets/activity_status.dart new file mode 100644 index 00000000..d22c307a --- /dev/null +++ b/lib/modules/tautulli/modules/activity/widgets/activity_status.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:tautulli/tautulli.dart'; +import 'package:lunasea/core.dart'; + +class TautulliActivityStatus extends StatelessWidget { + final TautulliActivity activity; + + TautulliActivityStatus({ + @required this.activity, + Key key, + }): super(key: key); + + @override + Widget build(BuildContext context) => LSHeader( + text: '${activity.streamCount} ${activity.streamCount == 1 ? 'Session' : 'Sessions'}', + subtitle: 'Total Bandwidth Usage: ${activity.totalBandwidth.lsBytes_KilobytesToString(bytes: false, decimals: 1)}ps', + ); +} diff --git a/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart b/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart new file mode 100644 index 00000000..2b31f67c --- /dev/null +++ b/lib/modules/tautulli/modules/activity/widgets/activity_tile.dart @@ -0,0 +1,206 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:tautulli/tautulli.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliActivityTile extends StatelessWidget { + final TautulliSession session; + final bool disableOnTap; + final double _height = 105.0; + final double _width = 70.0; + final double _padding = 8.0; + + TautulliActivityTile({ + Key key, + @required this.session, + this.disableOnTap = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) => LSCard( + child: disableOnTap + ? _body(context) + : InkWell( + child: _body(context), + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + onTap: () async => _enterDetails(context), + ), + decoration: LSCardBackground( + uri: Provider.of(context, listen: false).getImageURLFromPath(session.art), + headers: Provider.of(context, listen: false).headers, + darken: true, + ), + ); + + Widget _body(BuildContext context) => Row( + children: [ + _poster(context), + Expanded(child: _mediaInfo), + ], + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + ); + + String _artworkPath(BuildContext context) { + TautulliState _state = Provider.of(context, listen: false); + switch(session.mediaType) { + case TautulliMediaType.EPISODE: return _state.getImageURLFromRatingKey(session.grandparentRatingKey); + case TautulliMediaType.TRACK: return _state.getImageURLFromRatingKey(session.parentRatingKey); + case TautulliMediaType.MOVIE: + case TautulliMediaType.LIVE: + default: return _state.getImageURLFromRatingKey(session.ratingKey); + } + } + + Widget get _mediaInfo => Padding( + child: Container( + child: Column( + children: [ + _title, + _subtitle, + _transcodeDecision, + _user, + _progressBar, + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + ), + height: (_height-(_padding*2)), + ), + padding: EdgeInsets.all(_padding), + ); + + Widget get _title => LSTitle( + text: session.grandparentTitle == null || session.grandparentTitle.isEmpty + ? session.parentTitle == null || session.parentTitle.isEmpty + ? session.title == null || session.title.isEmpty + ? 'Unknown Title' + : session.title + : session.parentTitle + : session.grandparentTitle, + maxLines: 1, + ); + + Widget get _subtitle => RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + fontSize: Constants.UI_FONT_SIZE_SUBTITLE, + ), + children: [ + // Television + if(session.mediaType == TautulliMediaType.EPISODE) TextSpan(text: 'S${session.parentMediaIndex}\t•\tE${session.mediaIndex}'), + if(session.mediaType == TautulliMediaType.EPISODE) TextSpan(text: '\t—\t${session.title}'), + // Movie + if(session.mediaType == TautulliMediaType.MOVIE) TextSpan(text: session.year.toString()), + // Music + if(session.mediaType == TautulliMediaType.TRACK) TextSpan(text: session.title), + if(session.mediaType == TautulliMediaType.TRACK) TextSpan(text: '\t—\t${session.parentTitle}'), + // Live + if(session.mediaType == TautulliMediaType.LIVE) TextSpan(text: session.title), + ], + ), + softWrap: false, + maxLines: 1, + overflow: TextOverflow.fade, + ); + + Widget get _transcodeDecision { + String _value = ''; + switch(session.transcodeDecision) { + case TautulliTranscodeDecision.TRANSCODE: + String _transcodeStatus = session.transcodeThrottled ? 'Throttled' : '${session.transcodeSpeed ?? 0.0}x'; + _value = 'Transcoding ($_transcodeStatus)'; + break; + case TautulliTranscodeDecision.DIRECT_PLAY: + case TautulliTranscodeDecision.COPY: + case TautulliTranscodeDecision.NULL: _value = session.transcodeDecision.name ?? 'Unknown'; break; + } + return LSSubtitle(text: _value, maxLines: 1); + } + + Widget _poster(BuildContext context) => CachedNetworkImage( + fadeInDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + fadeOutDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + imageUrl: _artworkPath(context), + httpHeaders: Provider.of(context, listen: false).headers.cast(), + imageBuilder: (context, imageProvider) => Container( + height: _height, + width: _width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + placeholder: (context, url) => _placeholder, + errorWidget: (context, url, error) => _placeholder, + ); + + Widget get _placeholder => Container( + height: _height, + width: _width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + image: DecorationImage( + image: AssetImage('assets/images/radarr/nomovieposter.png'), + fit: BoxFit.cover, + ), + ), + ); + + Widget get _user { + IconData _icon; + switch(session.state) { + case TautulliSessionState.PAUSED: _icon = Icons.pause; break; + case TautulliSessionState.PLAYING: _icon = Icons.play_arrow; break; + case TautulliSessionState.BUFFERING: + default: _icon = Icons.compare_arrows; break; + } + return Row( + children: [ + LSIcon( + icon: _icon, + size: Constants.UI_FONT_SIZE_SUBTITLE, + color: Colors.white70, + ), + LSSubtitle(text: '\t${session.friendlyName}'), + ], + ); + } + + Widget get _progressBar => Padding( + child: Stack( + children: [ + LinearPercentIndicator( + percent: min(1.0, max(0, session.transcodeProgress/100)), + padding: EdgeInsets.symmetric(horizontal: 2.0), + progressColor: LSColors.splash.withOpacity(0.30), + backgroundColor: Colors.transparent, + lineHeight: 4.0, + ), + LinearPercentIndicator( + percent: min(1.0, max(0, session.progressPercent/100)), + padding: EdgeInsets.symmetric(horizontal: 2.0), + progressColor: LSColors.accent, + backgroundColor: LSColors.accent.withOpacity(0.15), + lineHeight: 4.0, + ), + ], + ), + padding: EdgeInsets.only(top: _padding), + ); + + Future _enterDetails(BuildContext context) async => Navigator.of(context).pushNamed( + TautulliActivityDetailsRoute.ROUTE_NAME, + arguments: TautulliActivityDetailsRouteArguments( + session: session, + ), + ); +} diff --git a/lib/modules/tautulli/modules/activity_details.dart b/lib/modules/tautulli/modules/activity_details.dart new file mode 100644 index 00000000..16bd30ba --- /dev/null +++ b/lib/modules/tautulli/modules/activity_details.dart @@ -0,0 +1,2 @@ +export 'activity_details/route.dart'; +export 'activity_details/widgets.dart'; diff --git a/lib/modules/tautulli/modules/activity_details/route.dart b/lib/modules/tautulli/modules/activity_details/route.dart new file mode 100644 index 00000000..a0585db9 --- /dev/null +++ b/lib/modules/tautulli/modules/activity_details/route.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliActivityDetailsRouteArguments { + final TautulliSession session; + + TautulliActivityDetailsRouteArguments({ + @required this.session, + }); +} + +class TautulliActivityDetailsRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/activity/details'; + + TautulliActivityDetailsRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + TautulliActivityDetailsRouteArguments _arguments; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((_) { + setState(() => _arguments = ModalRoute.of(context).settings.arguments); + }); + } + + @override + Widget build(BuildContext context) => _arguments == null + ? Scaffold() + : Scaffold( + key: _scaffoldKey, + appBar: _appBar, + body: _body, + ); + + Widget get _appBar => LSAppBar( + title: _arguments.session.title, + actions: [ + TautulliActivityDetailsTerminateSession(session: _arguments.session), + ], + ); + + Widget get _body => TautulliActivityDetailsInformation(session: _arguments.session); +} diff --git a/lib/modules/tautulli/modules/activity_details/widgets.dart b/lib/modules/tautulli/modules/activity_details/widgets.dart new file mode 100644 index 00000000..bcd30dcb --- /dev/null +++ b/lib/modules/tautulli/modules/activity_details/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/information.dart'; +export 'widgets/terminate_session.dart'; diff --git a/lib/modules/tautulli/modules/activity_details/widgets/information.dart b/lib/modules/tautulli/modules/activity_details/widgets/information.dart new file mode 100644 index 00000000..81649507 --- /dev/null +++ b/lib/modules/tautulli/modules/activity_details/widgets/information.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliActivityDetailsInformation extends StatelessWidget { + final TautulliSession session; + + TautulliActivityDetailsInformation({ + Key key, + @required this.session, + }): super(key: key); + + @override + Widget build(BuildContext context) => LSListView( + children: [ + TautulliActivityTile(session: session, disableOnTap: true), + ..._metadataBlock, + ..._playerBlock, + ..._streamBlock, + ], + ); + + List get _metadataBlock => _block( + title: 'Metadata', + children: [ + _title, + _content('year', session.year.toString()), + _duration, + _content('library', session.libraryName), + _content('user', session.friendlyName), + ] + ); + + Widget get _title => _content( + 'title', + [ + if(session.title != null && session.title.isNotEmpty) '${session.title}', + if(session.parentTitle != null && session.parentTitle.isNotEmpty) '\n${session.parentTitle}', + if(session.grandparentTitle != null && session.grandparentTitle.isNotEmpty) '\n${session.grandparentTitle}', + ].join() + ); + + Widget get _duration { + double _percent = session.progressPercent/100; + String _progress = Duration(seconds: (session.streamDuration.inSeconds*_percent).floor()).lsDuration_timestamp(); + String _total = session.streamDuration.lsDuration_timestamp(); + return _content('duration', '$_progress/$_total (${session.progressPercent}%)'); + } + + List get _playerBlock => _block( + title: 'Player', + children: [ + _content('location', session.ipAddress), + _content('device', session.device), + _content('platform', session.platform), + _content('product', session.product), + _content('player', session.player), + _content('version', session.platformVersion), + ], + ); + + List get _streamBlock => _block( + title: 'Stream', + children: [ + _bandwidth, + _quality, + _stream, + _container, + _video, + _audio, + ], + ); + + Widget get _bandwidth => _content('bandwidth', '${session.bandwidth.lsBytes_KilobytesToString(bytes: false, decimals: 1)}ps'); + + Widget get _quality => _content('quality', '${session.qualityProfile} (${session.streamBitrate.lsBytes_KilobytesToString(bytes: false, decimals: 1)}ps)'); + + Widget get _stream { + String _value = ''; + switch(session.transcodeDecision) { + case TautulliTranscodeDecision.TRANSCODE: + String _transcodeStatus = session.transcodeThrottled ? 'Throttled' : '${session.transcodeSpeed ?? 0.0}x'; + _value = 'Transcode ($_transcodeStatus)'; + break; + case TautulliTranscodeDecision.COPY: _value = 'Direct Stream'; break; + case TautulliTranscodeDecision.DIRECT_PLAY: _value = 'Direct Play'; break; + case TautulliTranscodeDecision.NULL: _value = 'Unknown Transcode Decision'; break; + } + return _content('stream', _value); + } + + Widget get _container => _content( + 'container', + [ + session.streamContainerDecision.name, + ' (', + session.container.toUpperCase(), + if(session.transcodeContainer != null && session.transcodeContainer.isNotEmpty) '${Constants.TEXT_RARROW} ${session.transcodeContainer.toUpperCase()}', + ')', + ].join(), + ); + + Widget get _video => session.streamVideoDecision != null && session.streamVideoDecision != TautulliTranscodeDecision.NULL + ? _content( + 'video', + [ + session.videoDecision.name, + ' (', + session.videoCodec.toUpperCase(), + ' ', + session.videoFullResolution, + if(session.transcodeVideoCodec.isNotEmpty) ' ${Constants.TEXT_RARROW} ', + if(session.transcodeVideoCodec.isNotEmpty) session.transcodeVideoCodec.toUpperCase(), + if(session.transcodeVideoCodec.isNotEmpty) ' ${session.streamVideoFullResolution}', + ')', + ].join(), + ) + : Container(); + + Widget get _audio => session.streamAudioDecision != null && session.streamAudioDecision != TautulliTranscodeDecision.NULL + ? _content( + 'audio', + [ + session.audioDecision.name, + ' (', + session.audioCodec.toUpperCase(), + if(session.transcodeAudioCodec.isNotEmpty) ' ${Constants.TEXT_RARROW} ', + if(session.transcodeAudioCodec.isNotEmpty) session.transcodeAudioCodec.toUpperCase(), + ')', + ].join(), + ) + : Container(); + + List _block({ + @required String title, + @required List children, + }) => [ + LSHeader(text: title), + LSCard( + child: Padding( + child: Column( + children: children, + ), + padding: EdgeInsets.symmetric(vertical: 8.0), + ), + ), + ]; + + Widget _content(String header, String body) => Padding( + child: Row( + children: [ + Expanded( + child: Text( + header.toUpperCase(), + textAlign: TextAlign.end, + style: TextStyle( + color: Colors.white70, + ), + ), + flex: 2, + ), + Container(width: 16.0, height: 0.0), + Expanded( + child: Text( + body, + textAlign: TextAlign.start, + style: TextStyle(), + ), + flex: 5, + ), + ], + crossAxisAlignment: CrossAxisAlignment.start, + ), + padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + ); +} diff --git a/lib/modules/tautulli/modules/activity_details/widgets/terminate_session.dart b/lib/modules/tautulli/modules/activity_details/widgets/terminate_session.dart new file mode 100644 index 00000000..b9f470b8 --- /dev/null +++ b/lib/modules/tautulli/modules/activity_details/widgets/terminate_session.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliActivityDetailsTerminateSession extends StatelessWidget { + final TautulliSession session; + + TautulliActivityDetailsTerminateSession({ + Key key, + @required this.session, + }): super(key: key); + + @override + Widget build(BuildContext context) => LSIconButton( + icon: Icons.stop, + onPressed: () async => _onPressed(context), + ); + + Future _onPressed(BuildContext context) async { + List _values = await TautulliDialogs.terminateSession(context); + if(_values[0]) { + Provider.of(context, listen: false).api.activity.terminateSession( + sessionKey: session.sessionKey, + message: _values[1] != null && (_values[1] as String).isNotEmpty ? _values[1] : null, + ) + .then((_) { + LSSnackBar( + context: context, + title: 'Terminated Session', + message: '${session.friendlyName}\t${Constants.TEXT_EMDASH}\t${session.title}', + ); + Navigator.of(context).pop(); + }) + .catchError((error, trace) { + Logger.error( + 'TautulliActivityDetailsTerminateSession', + '_onPressed', + 'Unable to terminate session: ${session.sessionKey}', + error, + trace, + uploadToSentry: !(error is DioError), + ); + LSSnackBar( + context: context, + title: 'Failed to Terminate Session', + type: SNACKBAR_TYPE.failure, + ); + }); + } + } +} \ No newline at end of file diff --git a/lib/modules/tautulli/modules/history.dart b/lib/modules/tautulli/modules/history.dart new file mode 100644 index 00000000..a89dfef1 --- /dev/null +++ b/lib/modules/tautulli/modules/history.dart @@ -0,0 +1,2 @@ +export 'history/route.dart'; +export 'history/widgets.dart'; diff --git a/lib/modules/tautulli/modules/history/route.dart b/lib/modules/tautulli/modules/history/route.dart new file mode 100644 index 00000000..85145f30 --- /dev/null +++ b/lib/modules/tautulli/modules/history/route.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliHistoryRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/history'; + + TautulliHistoryRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => LSGenericMessage(text: 'Coming Soon'); +} diff --git a/lib/modules/tautulli/modules/history/widgets.dart b/lib/modules/tautulli/modules/history/widgets.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/tautulli/modules/logs.dart b/lib/modules/tautulli/modules/logs.dart new file mode 100644 index 00000000..1de871a3 --- /dev/null +++ b/lib/modules/tautulli/modules/logs.dart @@ -0,0 +1,2 @@ +export 'logs/route.dart'; +export 'logs/widgets.dart'; diff --git a/lib/modules/tautulli/modules/logs/route.dart b/lib/modules/tautulli/modules/logs/route.dart new file mode 100644 index 00000000..c2cec1c5 --- /dev/null +++ b/lib/modules/tautulli/modules/logs/route.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliLogsRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/logs'; + + TautulliLogsRoute({ + Key key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + @override + Widget build(BuildContext context) => Scaffold( + key: _scaffoldKey, + appBar: _appBar, + body: _body, + ); + + Widget get _appBar => LSAppBar(title: 'Tautulli Logs'); + + Widget get _body => LSListView( + children: [ + TautulliLogsTautulliTile(), + TautulliLogsPlexMediaServerTile(), + TautulliLogsPlexMediaScannerTile(), + TautulliLogsNewslettersTile(), + TautulliLogsNotificationsTile(), + TautulliLogsLoginsTile(), + ], + ); +} diff --git a/lib/modules/tautulli/modules/logs/widgets.dart b/lib/modules/tautulli/modules/logs/widgets.dart new file mode 100644 index 00000000..fb9fe900 --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets.dart @@ -0,0 +1,6 @@ +export 'widgets/logins_tile.dart'; +export 'widgets/newsletters_tile.dart'; +export 'widgets/notifications_tile.dart'; +export 'widgets/plex_media_scanner.dart'; +export 'widgets/plex_media_server_tile.dart'; +export 'widgets/tautulli_tile.dart'; diff --git a/lib/modules/tautulli/modules/logs/widgets/logins_tile.dart b/lib/modules/tautulli/modules/logs/widgets/logins_tile.dart new file mode 100644 index 00000000..3fcbf822 --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets/logins_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsLoginsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Logins'), + subtitle: LSSubtitle(text: 'Tautulli Login Logs'), + leading: LSIconButton( + icon: Icons.vpn_key, + color: LSColors.list(5), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/logs/widgets/newsletters_tile.dart b/lib/modules/tautulli/modules/logs/widgets/newsletters_tile.dart new file mode 100644 index 00000000..9512c29a --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets/newsletters_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsNewslettersTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Newsletters'), + subtitle: LSSubtitle(text: 'Tautulli Newsletter Logs'), + leading: LSIconButton( + icon: Icons.email, + color: LSColors.list(3), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/logs/widgets/notifications_tile.dart b/lib/modules/tautulli/modules/logs/widgets/notifications_tile.dart new file mode 100644 index 00000000..eb28435e --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets/notifications_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsNotificationsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Notifications'), + subtitle: LSSubtitle(text: 'Tautulli Notification Logs'), + leading: LSIconButton( + icon: Icons.notifications, + color: LSColors.list(4), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/logs/widgets/plex_media_scanner.dart b/lib/modules/tautulli/modules/logs/widgets/plex_media_scanner.dart new file mode 100644 index 00000000..885f6415 --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets/plex_media_scanner.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsPlexMediaScannerTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Plex Media Scanner'), + subtitle: LSSubtitle(text: 'Plex Media Scanner Logs'), + leading: LSIconButton( + icon: Icons.scanner, + color: LSColors.list(2), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/logs/widgets/plex_media_server_tile.dart b/lib/modules/tautulli/modules/logs/widgets/plex_media_server_tile.dart new file mode 100644 index 00000000..01eae8ed --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets/plex_media_server_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsPlexMediaServerTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Plex Media Server'), + subtitle: LSSubtitle(text: 'Plex Media Server Logs'), + leading: LSIconButton( + icon: Icons.supervised_user_circle, + color: LSColors.list(1), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/logs/widgets/tautulli_tile.dart b/lib/modules/tautulli/modules/logs/widgets/tautulli_tile.dart new file mode 100644 index 00000000..64f67bb4 --- /dev/null +++ b/lib/modules/tautulli/modules/logs/widgets/tautulli_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliLogsTautulliTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Tautulli'), + subtitle: LSSubtitle(text: 'Tautulli Logs'), + leading: LSIconButton( + icon: CustomIcons.tautulli, + color: LSColors.list(0), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/more.dart b/lib/modules/tautulli/modules/more.dart new file mode 100644 index 00000000..28884860 --- /dev/null +++ b/lib/modules/tautulli/modules/more.dart @@ -0,0 +1,2 @@ +export 'more/route.dart'; +export 'more/widgets.dart'; diff --git a/lib/modules/tautulli/modules/more/route.dart b/lib/modules/tautulli/modules/more/route.dart new file mode 100644 index 00000000..325af056 --- /dev/null +++ b/lib/modules/tautulli/modules/more/route.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +//import 'package:lunasea/modules/tautulli.dart'; + +class TautulliMoreRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/more'; + + TautulliMoreRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => LSGenericMessage(text: 'Coming Soon'); + + // Widget get _body => LSListView( + // children: [ + // TautulliMoreGraphsTile(), + // TautulliMoreLibrariesTile(), + // TautulliMoreLogsTile(), + // TautulliMoreRecentlyAddedTile(), + // TautulliMoreStatisticsTile(), + // TautulliMoreSyncedItemsTile(), + // ], + // ); +} diff --git a/lib/modules/tautulli/modules/more/widgets.dart b/lib/modules/tautulli/modules/more/widgets.dart new file mode 100644 index 00000000..aa32265c --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets.dart @@ -0,0 +1,6 @@ +export 'widgets/graphs_tile.dart'; +export 'widgets/libraries_tile.dart'; +export 'widgets/logs_tile.dart'; +export 'widgets/recently_added_tile.dart'; +export 'widgets/statistics_tile.dart'; +export 'widgets/synced_items_tile.dart'; diff --git a/lib/modules/tautulli/modules/more/widgets/graphs_tile.dart b/lib/modules/tautulli/modules/more/widgets/graphs_tile.dart new file mode 100644 index 00000000..7b3ba2f1 --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets/graphs_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliMoreGraphsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Graphs'), + subtitle: LSSubtitle(text: 'Play Count & Play Duration Graphs'), + leading: LSIconButton( + icon: Icons.show_chart, + color: LSColors.list(0), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/more/widgets/libraries_tile.dart b/lib/modules/tautulli/modules/more/widgets/libraries_tile.dart new file mode 100644 index 00000000..4a6a08e2 --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets/libraries_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliMoreLibrariesTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Libraries'), + subtitle: LSSubtitle(text: 'Your Plex Library Metadata'), + leading: LSIconButton( + icon: Icons.video_library, + color: LSColors.list(1), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/more/widgets/logs_tile.dart b/lib/modules/tautulli/modules/more/widgets/logs_tile.dart new file mode 100644 index 00000000..acdd8d4d --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets/logs_tile.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliMoreLogsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Logs'), + subtitle: LSSubtitle(text: 'Tautulli & Plex Logs'), + leading: LSIconButton( + icon: Icons.developer_mode, + color: LSColors.list(2), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => Navigator.of(context).pushNamed(TautulliLogsRoute.ROUTE_NAME); +} diff --git a/lib/modules/tautulli/modules/more/widgets/recently_added_tile.dart b/lib/modules/tautulli/modules/more/widgets/recently_added_tile.dart new file mode 100644 index 00000000..662f7044 --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets/recently_added_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliMoreRecentlyAddedTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Recently Added'), + subtitle: LSSubtitle(text: 'Recently Added Content to Plex'), + leading: LSIconButton( + icon: Icons.recent_actors, + color: LSColors.list(3), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/more/widgets/statistics_tile.dart b/lib/modules/tautulli/modules/more/widgets/statistics_tile.dart new file mode 100644 index 00000000..5f2619c9 --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets/statistics_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliMoreStatisticsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Statistics'), + subtitle: LSSubtitle(text: 'User & Library Statistics'), + leading: LSIconButton( + icon: Icons.format_list_numbered, + color: LSColors.list(4), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/more/widgets/synced_items_tile.dart b/lib/modules/tautulli/modules/more/widgets/synced_items_tile.dart new file mode 100644 index 00000000..10083006 --- /dev/null +++ b/lib/modules/tautulli/modules/more/widgets/synced_items_tile.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class TautulliMoreSyncedItemsTile extends StatelessWidget { + @override + Widget build(BuildContext context) => LSCardTile( + title: LSTitle(text: 'Synced Items'), + subtitle: LSSubtitle(text: 'Synced Content on Devices'), + leading: LSIconButton( + icon: Icons.sync, + color: LSColors.list(5), + ), + onTap: () async => _onTap(context), + ); + + Future _onTap(BuildContext context) async => LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); +} diff --git a/lib/modules/tautulli/modules/tautulli.dart b/lib/modules/tautulli/modules/tautulli.dart new file mode 100644 index 00000000..10e662fd --- /dev/null +++ b/lib/modules/tautulli/modules/tautulli.dart @@ -0,0 +1,2 @@ +export 'tautulli/route.dart'; +export 'tautulli/widgets.dart'; diff --git a/lib/modules/tautulli/modules/tautulli/route.dart b/lib/modules/tautulli/modules/tautulli/route.dart new file mode 100644 index 00000000..43bc0fca --- /dev/null +++ b/lib/modules/tautulli/modules/tautulli/route.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class Tautulli extends StatefulWidget { + static const ROUTE_NAME = '/tautulli'; + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + PageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: Provider.of(context, listen: false).navigationIndex); + } + + @override + Widget build(BuildContext context) => WillPopScope( + onWillPop: _willPopScope, + child: Scaffold( + key: _scaffoldKey, + drawer: _drawer, + appBar: _appBar, + bottomNavigationBar: _bottomNavigationBar, + body: _body, + ), + ); + + Future _willPopScope() async { + if(_scaffoldKey.currentState.isDrawerOpen) { + return true; + } else { + _scaffoldKey.currentState.openDrawer(); + return false; + } + } + + Widget get _drawer => LSDrawer(page: 'tautulli'); + + Widget get _bottomNavigationBar => TautulliNavigationBar(pageController: _pageController); + + List get _tabs => [ + TautulliActivityRoute(), + TautulliUsersRoute(), + TautulliHistoryRoute(), + TautulliMoreRoute(), + ]; + + Widget get _body => Selector( + selector: (_, state) => state.enabled, + builder: (context, enabled, _) => PageView( + controller: _pageController, + children: enabled ? _tabs : List.generate(_tabs.length, (_) => LSNotEnabled('Tautulli')), + onPageChanged: _onPageChanged, + ), + ); + + Widget get _appBar => LSAppBarDropdown( + context: context, + title: 'Tautulli', + profiles: Database.profilesBox.keys.fold([], (value, element) { + if((Database.profilesBox.get(element) as ProfileHiveObject).tautulliEnabled) value.add(element); + return value; + }), + actions: Provider.of(context).enabled ? [TautulliGlobalSettings()] : null, + ); + + void _onPageChanged(int index) => Provider.of(context, listen: false).navigationIndex = index; +} diff --git a/lib/modules/tautulli/modules/tautulli/widgets.dart b/lib/modules/tautulli/modules/tautulli/widgets.dart new file mode 100644 index 00000000..99990c95 --- /dev/null +++ b/lib/modules/tautulli/modules/tautulli/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/global_settings.dart'; +export 'widgets/navigation_bar.dart'; diff --git a/lib/modules/tautulli/modules/tautulli/widgets/global_settings.dart b/lib/modules/tautulli/modules/tautulli/widgets/global_settings.dart new file mode 100644 index 00000000..3351b8e0 --- /dev/null +++ b/lib/modules/tautulli/modules/tautulli/widgets/global_settings.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliGlobalSettings extends StatelessWidget { + @override + Widget build(BuildContext context) => LSIconButton( + icon: Icons.more_vert, + onPressed: () async => _handler(context), + ); + + Future _handler(BuildContext context) async { + List values = await TautulliDialogs.globalSettings(context); + if(values[0]) switch(values[1] as TautulliGlobalSettingsType) { + case TautulliGlobalSettingsType.WEB_GUI: _webGUI(context); break; + case TautulliGlobalSettingsType.BACKUP_CONFIG: _backupConfig(context); break; + case TautulliGlobalSettingsType.BACKUP_DB: _backupDB(context); break; + case TautulliGlobalSettingsType.DELETE_CACHE: _deleteCache(context); break; + case TautulliGlobalSettingsType.DELETE_IMAGE_CACHE: _deleteImageCache(context); break; + default: Logger.warning('TautulliGlobalSettings', '_handler', 'Unknown case: ${(values[1] as TautulliGlobalSettings)}'); + } + } + + Future _webGUI(BuildContext context) async => Provider.of(context, listen: false).host.lsLinks_OpenLink(); + + Future _backupConfig(BuildContext context) async { + Provider.of(context, listen: false).api.system.backupConfig() + .then((_) => LSSnackBar( + context: context, + title: 'Backing Up Configuration...', + message: 'Backing up your configuration in the background', + )) + .catchError((error, trace) { + Logger.error( + 'Tautulli', + '_backupConfig', + 'Failed to backup configuration', + error, + trace, + uploadToSentry: !(error is DioError), + ); + LSSnackBar( + context: context, + title: 'Failed to Backup Configuration', + type: SNACKBAR_TYPE.failure, + ); + }); + } + + Future _backupDB(BuildContext context) async { + Provider.of(context, listen: false).api.system.backupDB() + .then((_) => LSSnackBar( + context: context, + title: 'Backing Up Database...', + message: 'Backing up your database in the background', + )) + .catchError((error, trace) { + Logger.error( + 'Tautulli', + '_backupDB', + 'Failed to backup database', + error, + trace, + uploadToSentry: !(error is DioError), + ); + LSSnackBar( + context: context, + title: 'Failed to Backup Database', + type: SNACKBAR_TYPE.failure, + ); + }); + } + + Future _deleteCache(BuildContext context) async { + Provider.of(context, listen: false).api.system.deleteCache() + .then((_) => LSSnackBar( + context: context, + title: 'Deleting Cache...', + message: 'Tautulli cache is being deleted', + )) + .catchError((error, trace) { + Logger.error( + 'Tautulli', + '_deleteCache', + 'Failed to delete cache', + error, + trace, + uploadToSentry: !(error is DioError), + ); + LSSnackBar( + context: context, + title: 'Failed to Delete Cache', + type: SNACKBAR_TYPE.failure, + ); + }); + } + + Future _deleteImageCache(BuildContext context) async { + Provider.of(context, listen: false).api.system.deleteImageCache() + .then((_) => LSSnackBar( + context: context, + title: 'Deleting Image Cache...', + message: 'Tautulli image cache is being deleted', + )) + .catchError((error, trace) { + Logger.error( + 'Tautulli', + '_deleteImageCache', + 'Failed to delete image cache', + error, + trace, + uploadToSentry: !(error is DioError), + ); + LSSnackBar( + context: context, + title: 'Failed to Delete Image Cache', + type: SNACKBAR_TYPE.failure, + ); + }); + } +} \ No newline at end of file diff --git a/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart b/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart new file mode 100644 index 00000000..525706d7 --- /dev/null +++ b/lib/modules/tautulli/modules/tautulli/widgets/navigation_bar.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; + +class TautulliNavigationBar extends StatefulWidget { + static const List icons = [ + CustomIcons.monitoring, + CustomIcons.user, + CustomIcons.history, + Icons.more_horiz, + ]; + + static const List titles = [ + 'Activity', + 'Users', + 'History', + 'More', + ]; + + final PageController pageController; + + TautulliNavigationBar({ + Key key, + @required this.pageController, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) => Selector( + selector: (_, model) => model.navigationIndex, + builder: (context, index, _) => LSBottomNavigationBar( + index: index, + icons: TautulliNavigationBar.icons, + titles: TautulliNavigationBar.titles, + onTap: _navOnTap, + ), + ); + + Future _navOnTap(int index) async { + await widget.pageController.animateToPage( + index, + duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + curve: Curves.easeOutSine, + ); + Provider.of(context, listen: false).navigationIndex = index; + } +} \ No newline at end of file diff --git a/lib/modules/tautulli/modules/users.dart b/lib/modules/tautulli/modules/users.dart new file mode 100644 index 00000000..da4b1d89 --- /dev/null +++ b/lib/modules/tautulli/modules/users.dart @@ -0,0 +1,2 @@ +export 'users/route.dart'; +export 'users/widgets.dart'; diff --git a/lib/modules/tautulli/modules/users/route.dart b/lib/modules/tautulli/modules/users/route.dart new file mode 100644 index 00000000..96dcb21c --- /dev/null +++ b/lib/modules/tautulli/modules/users/route.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliUsersRoute extends StatefulWidget { + static const ROUTE_NAME = '/tautulli/users'; + + TautulliUsersRoute({ + Key key, + }): super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + Future _refresh() async { + TautulliState _state = Provider.of(context, listen: false); + _state.resetUsers(); + await _state.users; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + key: _scaffoldKey, + body: _body, + ); + } + + Widget get _body => LSRefreshIndicator( + refreshKey: _refreshKey, + onRefresh: _refresh, + child: Selector>( + selector: (_, state) => state.users, + builder: (context, users, _) => FutureBuilder( + future: users, + builder: (context, snapshot) { + if(snapshot.hasError) { + if(snapshot.connectionState != ConnectionState.waiting) { + Logger.error( + 'TautulliUsersRoute', + '_body', + 'Unable to fetch Tautulli users', + snapshot.error, + null, + uploadToSentry: !(snapshot.error is DioError), + ); + } + return LSErrorMessage(onTapHandler: () async => _refresh()); + } + if(snapshot.hasData) return (snapshot.data as TautulliUsersTable).users.length == 0 + ? _noUsers() + : _list((snapshot.data as TautulliUsersTable)); + return LSLoader(); + }, + ), + ), + ); + + Widget _list(TautulliUsersTable users) => LSListViewBuilder( + itemCount: users.users.length, + itemBuilder: (context, index) => TautulliUserTile(user: users.users[index]), + ); + + Widget _noUsers() => LSGenericMessage( + text: 'No Users Found', + showButton: true, + buttonText: 'Refresh', + onTapHandler: () async => _refresh(), + ); +} diff --git a/lib/modules/tautulli/modules/users/widgets.dart b/lib/modules/tautulli/modules/users/widgets.dart new file mode 100644 index 00000000..11486c43 --- /dev/null +++ b/lib/modules/tautulli/modules/users/widgets.dart @@ -0,0 +1 @@ +export 'widgets/user_tile.dart'; diff --git a/lib/modules/tautulli/modules/users/widgets/user_tile.dart b/lib/modules/tautulli/modules/users/widgets/user_tile.dart new file mode 100644 index 00000000..41be7dda --- /dev/null +++ b/lib/modules/tautulli/modules/users/widgets/user_tile.dart @@ -0,0 +1,135 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/tautulli.dart'; +import 'package:tautulli/tautulli.dart'; + +class TautulliUserTile extends StatelessWidget { + final TautulliTableUser user; + final double _imageDimension = 70.0; + final double _padding = 8.0; + + TautulliUserTile({ + Key key, + @required this.user, + }) : super(key: key); + + @override + Widget build(BuildContext context) => LSCard( + child: InkWell( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + child: Row( + children: [ + _userThumb(context), + _details, + ], + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + ), + onTap: () async => _enterDetails(context), + ), + decoration: LSCardBackground( + darken: true, + uri: Provider.of(context, listen: false).getImageURLFromPath(user.thumb ?? ''), + headers: Provider.of(context, listen: false).headers.cast(), + ), + ); + + Widget _userThumb(BuildContext context) => CachedNetworkImage( + fadeInDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + fadeOutDuration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED), + imageUrl: Provider.of(context, listen: false).getImageURLFromPath(user.userThumb), + httpHeaders: Provider.of(context, listen: false).headers.cast(), + imageBuilder: (context, imageProvider) => Container( + height: _imageDimension, + width: _imageDimension, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + placeholder: (context, url) => _placeholder, + errorWidget: (context, url, error) => _placeholder, + ); + + Widget get _placeholder => Container( + height: _imageDimension, + width: _imageDimension, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS), + image: DecorationImage( + image: AssetImage('assets/images/tautulli/nouserthumb.png'), + fit: BoxFit.cover, + ), + ), + ); + + Widget get _details => Expanded( + child: Padding( + child: Container( + child: Column( + children: [ + _title, + _lastPlayedTime, + _lastPlayedContent, + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + ), + height: (_imageDimension-(_padding*2)), + ), + padding: EdgeInsets.all(_padding), + ), + ); + + Widget get _title => LSTitle(text: user.friendlyName); + + Widget get _lastPlayedTime => Row( + children: [ + Padding( + child: LSIcon( + icon: Icons.visibility, + size: Constants.UI_FONT_SIZE_SUBTITLE, + ), + padding: EdgeInsets.only(right: 6.0), + ), + Expanded( + child: LSSubtitle( + text: user.lastSeen != null ? DateTime.now().lsDateTime_ageString(user.lastSeen) : 'Never', + maxLines: 1, + ), + ), + ], + ); + + Widget get _lastPlayedContent => Row( + children: [ + Padding( + child: LSIcon( + icon: Icons.play_arrow, + size: Constants.UI_FONT_SIZE_SUBTITLE, + ), + padding: EdgeInsets.only(right: 6.0), + ), + Expanded( + child: LSSubtitle( + text: user.lastPlayed ?? 'Never', + maxLines: 1, + ), + ), + ], + ); + + Future _enterDetails(BuildContext context) async { + LSSnackBar( + context: context, + title: 'Coming Soon!', + message: 'This feature has not yet been implemented', + type: SNACKBAR_TYPE.info, + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 9201ac8c..029d2abf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -349,6 +349,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.8" + flutter_spinkit: + dependency: "direct main" + description: + name: flutter_spinkit + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.2+1" flutter_sticky_header: dependency: "direct main" description: @@ -388,7 +395,7 @@ packages: name: google_nav_bar url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.0.0" graphs: dependency: transitive description: @@ -696,7 +703,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2" + version: "4.3.2+1" pub_semver: dependency: transitive description: @@ -848,7 +855,7 @@ packages: name: tautulli url: "https://pub.dartlang.org" source: hosted - version: "1.0.0+2" + version: "1.0.1" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d80b040b..abcfc3bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: lunasea description: Self Hosted Manager -version: 3.3.0+330002 +version: 4.0.0+400001 publish_to: 'none' environment: sdk: ">=2.7.0 <3.0.0" @@ -19,11 +19,12 @@ dependencies: flutter: sdk: flutter flutter_cache_manager: ^1.4.1 + flutter_spinkit: ^4.1.2+1 flutter_sticky_header: ^0.4.5 flutter_svg: ^0.18.0 f_logs: ^1.3.0-alpha-02 google_fonts: ^1.1.0 - google_nav_bar: ^2.2.0 + google_nav_bar: ^3.0.0 hive: ^1.4.3 hive_flutter: ^0.3.1 in_app_purchase: ^0.3.4+3 @@ -32,7 +33,7 @@ dependencies: path_provider: ^1.6.11 percent_indicator: ^2.1.5 permission_handler: ^5.0.1+1 - provider: ^4.3.2 + provider: ^4.3.2+1 sentry: ^3.0.1 stack_trace: ^1.9.5 table_calendar: ^2.2.3 @@ -41,7 +42,7 @@ dependencies: uuid: ^2.2.0 xml_parser: ^0.1.2 # LunaTools Packages - tautulli: ^1.0.0+2 + tautulli: ^1.0.1 wake_on_lan: ^1.1.1+1 dev_dependencies: @@ -62,6 +63,8 @@ flutter: - assets/images/lidarr/ - assets/images/radarr/ - assets/images/sonarr/ + # Monitoring + - assets/images/tautulli/ # Services - assets/images/services/ # Branding @@ -85,6 +88,6 @@ flutter_native_splash: color: "#32323E" scripts: - adapters: flutter packages pub run build_runner build --delete-conflicting-outputs + generate: flutter packages pub run build_runner build --delete-conflicting-outputs icons: flutter pub run flutter_launcher_icons:main splash: flutter pub pub run flutter_native_splash:create