[Release] v2.1.0 (66) (#200)

* [Icon] Removed alpha channel from icon
* [Sonarr] Fixed toast for season monitoring toggle
* [Settings] Completely redesign, prepping for user configurable options
* [Browsers] Ability to set which browser
* [Settings/Modules] Long pressing 'Open Links In...' would open LunaSea website
* [Navigation Bar] Fixed title regression
* [Sliver AppBar] Defaults to collapsed, pull down to view
* [Sliver AppBar] Left align title
* [Settings/Calendar] Ability to configure starting day and starting size
* [Settings] Added headers
* [Settings] Cleaned folder structure of widgets
* [Calendar] Added option to disable specific modules
* [Theme] added AMOLED black theme
* [Android] Build Configuration for Android (#198)
* [Android] Implemented WIP android build configuration
* [Android] Added splash, icons, dynamic platform-specific text
* [Android] Added signing configuration, updated minimum version to Android 7.0
* [README] Updated README to reflect Android building
* [README] Added stub Google Play shield
* [README] Reordered shields
This commit is contained in:
Jagandeep Brar
2020-04-20 10:12:16 -05:00
committed by GitHub
parent 5bd0126829
commit 8ac4228e56
202 changed files with 2469 additions and 1207 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ ios/build/
ExportOptions.plist
api_responses/
pre_archive
key.properties

View File

@@ -1,10 +1,14 @@
[![Homepage](https://img.shields.io/badge/Homepage-LunaSea-red?style=for-the-badge&logo=html5&color=%234ECCA3&logoColor=white)](https://www.lunasea.app)   [![Release](https://img.shields.io/badge/AppStore-v2.0.1%20(55)-red?style=for-the-badge&logo=app-store&color=%230D96F6&logoColor=white)](https://apps.apple.com/us/app/lunasea/id1496797802?ls=1)   [![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/LunaSeaApp?label=R%2FLunaSeaApp&logo=reddit&logoColor=white&style=for-the-badge)](https://www.reddit.com/r/LunaSeaApp)   [![License](https://img.shields.io/github/license/LunaSeaApp/LunaSea?color=%23222222&style=for-the-badge&logo=github&logoColor=white)](https://github.com/LunaSeaApp/LunaSea/blob/master/LICENSE)
[![Homepage](https://img.shields.io/badge/Homepage-LunaSea-red?style=for-the-badge&logo=html5&color=%234ECCA3&logoColor=white)](https://www.lunasea.app)
  [![Subreddit](https://img.shields.io/reddit/subreddit-subscribers/LunaSeaApp?label=R%2FLunaSeaApp&logo=reddit&logoColor=white&style=for-the-badge)](https://www.reddit.com/r/LunaSeaApp)
  [![Release](https://img.shields.io/badge/App%20Store-v2.1.0%20(66)-red?style=for-the-badge&logo=app-store&color=%230D96F6&logoColor=white)](https://apps.apple.com/us/app/lunasea/id1496797802?ls=1)
  [![Release](https://img.shields.io/badge/Google%20Play-v2.1.0%20(66)-red?style=for-the-badge&logo=google-play&color=%230F9D58&logoColor=white)](https://github.com/LunaSeaApp/LunaSea/wiki/Play-Store-Release)
  [![License](https://img.shields.io/github/license/LunaSeaApp/LunaSea?color=%23222222&style=for-the-badge&logo=github&logoColor=white)](https://github.com/LunaSeaApp/LunaSea/blob/master/LICENSE)
![LunaSea](https://www.lunasea.app/images/banner.png)
![Screenshot](https://www.lunasea.app/images/hero.png)
LunaSea is a fully-featured, open-source usenet manager built using the Flutter framework for iOS! LunaSea is focused on giving you a seamless experience between all of your usenet software.
LunaSea is a fully-featured, open-source usenet manager built using the Flutter framework for mobile devices! LunaSea is focused on giving you a seamless experience between all of your usenet software.
Support for services include:
- Lidarr
@@ -21,7 +25,7 @@ LunaSea also comes with support for:
---
## Developing, Installing, & Building
## Developing, Installing, & Building (iOS)
#### Requirements
@@ -61,3 +65,47 @@ LunaSea also comes with support for:
- `flutter pub get`
3. Run `build_ipa` inside of the `scripts` folder
4. The IPA will be placed in the root of the project directory
---
## Developing, Installing, & Building (Android)
#### Requirements
1. Android SDK/Android Studio Installed & Configured
2. [Flutter Framework (Beta Channel)](https://flutter.dev/)
3. [Keystore Configured](https://github.com/LunaSeaApp/LunaSea/wiki/Configure-Keystore)
#### Developing
1. Clone the repository
- `git clone git@github.com:LunaSeaApp/LunaSea.git`
2. Install the Flutter packages
- `flutter pub get`
3. Start your simulator or plug in your device and ensure you have enabled USB Debugging
4. Install LunaSea in development mode on your device or simulator
- `flutter run`
#### Installing
> Release builds can only be installed on physical devices
1. Clone the repository
- `git clone git@github.com:LunaSeaApp/LunaSea.git`
2. Install the Flutter packages
- `flutter pub get`
3. Plug in your device and ensure you have enabled USB Debugging
4. Install a production version of the application on your device
- `flutter run --release`
#### Building (.apk)
1. Clone the repository
- `git clone git@github.com:LunaSeaApp/LunaSea.git`
2. Install the Flutter packages
- `flutter pub get`
3. Run `flutter build apk --split-per-abi`
4. The APKs are located in:
- `./build/app/outputs/apk/release/app-armeabi-v7a-release.apk`
- `./build/app/outputs/apk/release/app-arm64-v8a-release.apk`
- `./build/app/outputs/apk/release/app-x86_64-release.apk`

7
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

75
android/app/build.gradle Normal file
View File

@@ -0,0 +1,75 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
compileSdkVersion 29
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
applicationId "app.lunasea.lunasea"
minSdkVersion 24
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -0,0 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.lunasea.lunasea">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>

View File

@@ -0,0 +1,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.lunasea.lunasea">
<application
android:name="io.flutter.app.FlutterApplication"
android:requestLegacyExternalStorage="true"
android:label="LunaSea"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>

View File

@@ -0,0 +1,9 @@
package app.lunasea.lunasea
import io.flutter.embedding.android.FlutterActivity
import android.os.Build
import android.view.ViewTreeObserver
import android.view.WindowManager
class MainActivity: FlutterActivity() {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_color" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="center" android:src="@drawable/splash" />
</item>
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_color">#32323E</color>
<color name="ic_launcher_background">#32323E</color>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_color</item>
<item name="android:windowFullscreen">false</item>
</style>
</resources>

View File

@@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.lunasea.lunasea">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>

31
android/build.gradle Normal file
View File

@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip

15
android/settings.gradle Normal file
View File

@@ -0,0 +1,15 @@
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

View File

@@ -0,0 +1 @@
include ':app'

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 810 B

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 809 B

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -4,11 +4,23 @@ import 'package:lunasea/core/database.dart';
class Export {
Export._();
static bool validateType(dynamic value) {
Type _type = value.runtimeType;
if(
_type == bool ||
_type == String ||
_type == int ||
_type == double
) return true;
return false;
}
static Map get _lunasea {
Box<dynamic> _box = Database.lunaSeaBox;
Map<String, dynamic> _data = {};
for(var key in _box.keys) {
_data[key] = _box.get(key);
dynamic _value = _box.get(key);
if(validateType(_value)) _data[key] = _value;
}
return _data;
}

View File

@@ -11,7 +11,9 @@ class Filesystem {
}
static Future<String> get appDirectory async {
Directory appDocDir = await getApplicationDocumentsDirectory();
Directory appDocDir;
if(Platform.isIOS) appDocDir = await getApplicationDocumentsDirectory();
if(Platform.isAndroid) appDocDir = await getExternalStorageDirectory();
return '${appDocDir.path}';
}
@@ -41,10 +43,10 @@ class Filesystem {
String _directory = await configDirectory;
//Create configuration folder if needed
Directory directory = Directory(_directory);
if(!await directory.exists()) await directory.create();
await directory.create(recursive: true);
//Write the configuration to the filsystem
File file = File(_file);
file?.writeAsString(config);
await file?.writeAsString(config);
}
static Future<void> exportDownloadToFilesystem(String name, String data) async {
@@ -52,9 +54,9 @@ class Filesystem {
String _directory = await downloadDirectory;
//Create download folder if needed
Directory directory = Directory(_directory);
if(!await directory.exists()) await directory.create();
await directory.create(recursive: true);
//Write the NZB to the filesystem
File file = File(_nzb);
file?.writeAsString(data);
await file?.writeAsString(data);
}
}

View File

@@ -6,7 +6,7 @@ class Import {
static void _setLunaSea(Map data) {
Box<dynamic> _box = Database.lunaSeaBox;
_box.put('profile', data['profile']);
_box.put(LunaSeaDatabaseValue.ENABLED_PROFILE.key, data['profile']);
}
static void _setProfiles(List data) {

View File

@@ -11,25 +11,25 @@ class Constants {
'sonarr': SonarrConstants.SERVICE_MAP,
'nzbget': {
'name': 'NZBGet',
'desc': 'Manage your downloads',
'desc': 'Manage Usenet Downloads',
'icon': CustomIcons.nzbget,
'route': '/nzbget',
},
'sabnzbd': {
'name': 'SABnzbd',
'desc': 'Manage your downloads',
'desc': 'Manage Usenet Downloads',
'icon': CustomIcons.sabnzbd,
'route': '/sabnzbd',
},
'search': {
'name': 'Search',
'desc': 'Search newznab indexers',
'desc': 'Search Newznab Indexers',
'icon': Icons.search,
'route': '/search',
},
'settings': {
'name': 'Settings',
'desc': 'Update your configuration',
'desc': 'Update Configuration',
'icon': Icons.settings,
'route': '/settings',
}

View File

@@ -1,2 +1,3 @@
export './adapters/indexer.dart';
export './adapters/lunasea.dart';
export './adapters/profile.dart';

View File

@@ -0,0 +1,37 @@
import 'package:lunasea/core/database/database.dart';
import 'package:lunasea/core/extensions.dart';
class LunaSeaDatabase {
LunaSeaDatabase._();
static void registerAdapters() {
Hive.registerAdapter(LSBrowsersAdapter());
}
}
enum LunaSeaDatabaseValue {
ENABLED_PROFILE,
THEME_AMOLED,
SELECTED_BROWSER,
}
extension LunaSeaDatabaseValueExtension on LunaSeaDatabaseValue {
String get key {
switch(this) {
case LunaSeaDatabaseValue.ENABLED_PROFILE: return 'profile';
case LunaSeaDatabaseValue.THEME_AMOLED: return 'LUNASEA_THEME_AMOLED';
case LunaSeaDatabaseValue.SELECTED_BROWSER: return 'LUNASEA_SELECTED_BROWSER';
default: return '';
}
}
dynamic get data {
final _box = Database.lunaSeaBox;
switch(this) {
case LunaSeaDatabaseValue.ENABLED_PROFILE: return _box.get(this.key, defaultValue: 'default');
case LunaSeaDatabaseValue.SELECTED_BROWSER: return _box.get(this.key, defaultValue: LSBrowsers.APPLE_SAFARI);
case LunaSeaDatabaseValue.THEME_AMOLED: return _box.get(this.key, defaultValue: false);
default: return null;
}
}
}

View File

@@ -21,7 +21,10 @@ class Database {
//Core
Hive.registerAdapter(IndexerHiveObjectAdapter());
Hive.registerAdapter(ProfileHiveObjectAdapter());
//Modules
//Modules - General
LunaSeaDatabase.registerAdapters();
HomeDatabase.registerAdapters();
//Modules - Automation
LidarrDatabase.registerAdapters();
RadarrDatabase.registerAdapters();
SonarrDatabase.registerAdapters();
@@ -40,7 +43,7 @@ class Database {
clearIndexersBox();
//Set default profile & enabled profile
profilesBox.put('default', ProfileHiveObject.empty());
lunaSeaBox.put('profile', 'default');
lunaSeaBox.put(LunaSeaDatabaseValue.ENABLED_PROFILE.key, 'default');
}

View File

@@ -1,17 +1,104 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core/database.dart';
import 'package:lunasea/core/ui.dart';
import 'package:url_launcher/url_launcher.dart';
part 'links.g.dart';
@HiveType(typeId: 11, adapterName: 'LSBrowsersAdapter')
enum LSBrowsers {
@HiveField(0)
APPLE_SAFARI,
@HiveField(1)
BRAVE_BROWSER,
@HiveField(2)
GOOGLE_CHROME,
@HiveField(3)
MICROSOFT_EDGE,
@HiveField(4)
MOZILLA_FIREFOX,
}
extension LSBrowsersExtension on LSBrowsers {
String get name {
switch(this) {
case LSBrowsers.APPLE_SAFARI: return 'Apple Safari';
case LSBrowsers.BRAVE_BROWSER: return 'Brave Browser';
case LSBrowsers.GOOGLE_CHROME: return 'Google Chrome';
case LSBrowsers.MICROSOFT_EDGE: return 'Microsoft Edge';
case LSBrowsers.MOZILLA_FIREFOX: return 'Mozilla Firefox';
default: return 'Unknown Browser';
}
}
String get key {
switch(this) {
case LSBrowsers.APPLE_SAFARI: return 'applesafari';
case LSBrowsers.BRAVE_BROWSER: return 'bravebrowser';
case LSBrowsers.GOOGLE_CHROME: return 'googlechrome';
case LSBrowsers.MICROSOFT_EDGE: return 'microsoftedge';
case LSBrowsers.MOZILLA_FIREFOX: return 'mozillafirefox';
default: return 'unknown';
}
}
IconData get icon {
switch(this) {
case LSBrowsers.APPLE_SAFARI: return CustomIcons.safari;
case LSBrowsers.BRAVE_BROWSER: return CustomIcons.bravebrowser;
case LSBrowsers.GOOGLE_CHROME: return CustomIcons.chrome;
case LSBrowsers.MICROSOFT_EDGE: return CustomIcons.microsoftedge;
case LSBrowsers.MOZILLA_FIREFOX: return CustomIcons.firefox;
default: return null;
}
}
String formatted(String url) {
bool isHttps = url.substring(0, 8) == 'https://';
switch(this) {
case LSBrowsers.BRAVE_BROWSER: return 'brave://open-url?url=$url';
case LSBrowsers.GOOGLE_CHROME: return isHttps
? url.replaceFirst('https://', 'googlechromes://')
: url.replaceFirst('http://', 'googlechrome://');
case LSBrowsers.MICROSOFT_EDGE: return isHttps
? url.replaceFirst('https://', 'microsoft-edge-https://')
: url.replaceFirst('http://', 'microsoft-edge-http://');
case LSBrowsers.MOZILLA_FIREFOX: return 'firefox://open-url?url=$url';
case LSBrowsers.APPLE_SAFARI:
default: return url;
}
}
}
extension StringLinksExtension on String {
Future<void> _openLink(String url) async {
try {
bool launched = await launch(
url,
forceSafariVC: false,
universalLinksOnly: true,
);
if(!launched) await launch(url);
//Attempt to launch the link forced as a universal link
if(await _launchUniversalLink(url)) return;
//If that fails and we're using a custom browser, format the link for the correct browser and attempt to launch
LSBrowsers _browser = LunaSeaDatabaseValue.SELECTED_BROWSER.data;
if(
_browser != LSBrowsers.APPLE_SAFARI &&
await _launchCustomBrowser(_browser.formatted(url))
) return;
//If all else fails, just launch it in Safari
await _launch(url);
} catch (e) {}
}
Future<bool> _launchUniversalLink(String url) async => await launch(
url,
forceSafariVC: false,
universalLinksOnly: true,
);
Future<bool> _launchCustomBrowser(String url) async => await launch(
url,
forceSafariVC: false,
);
Future<bool> _launch(String url) async => await launch(url);
// ignore: non_constant_identifier_names
Future<void> lsLinks_OpenLink() async => await _openLink(this);
// ignore: non_constant_identifier_names

View File

@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'links.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class LSBrowsersAdapter extends TypeAdapter<LSBrowsers> {
@override
final typeId = 11;
@override
LSBrowsers read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return LSBrowsers.APPLE_SAFARI;
case 1:
return LSBrowsers.BRAVE_BROWSER;
case 2:
return LSBrowsers.GOOGLE_CHROME;
case 3:
return LSBrowsers.MICROSOFT_EDGE;
case 4:
return LSBrowsers.MOZILLA_FIREFOX;
default:
return null;
}
}
@override
void write(BinaryWriter writer, LSBrowsers obj) {
switch (obj) {
case LSBrowsers.APPLE_SAFARI:
writer.writeByte(0);
break;
case LSBrowsers.BRAVE_BROWSER:
writer.writeByte(1);
break;
case LSBrowsers.GOOGLE_CHROME:
writer.writeByte(2);
break;
case LSBrowsers.MICROSOFT_EDGE:
writer.writeByte(3);
break;
case LSBrowsers.MOZILLA_FIREFOX:
writer.writeByte(4);
break;
}
}
}

View File

@@ -7,7 +7,6 @@ class Logger {
Logger._();
static void initialize() {
WidgetsFlutterBinding.ensureInitialized();
LogsConfig config = FLog.getDefaultConfigurations()
..formatType = FormatType.FORMAT_SQUARE
..timestampFormat = 'MMMM dd, y - hh:mm:ss a';

View File

@@ -12,6 +12,6 @@ class ModuleFlags {
static const SABNZBD = true;
static const NZBGET = true;
//Monitoring
static const MONITORING = true;
static const MONITORING = false;
static const TAUTULLI = true;
}

View File

@@ -26,118 +26,129 @@ class Routes {
static Map<String, WidgetBuilder> get _home => <String, WidgetBuilder> {
// /
Home.ROUTE_NAME: (BuildContext context) => Home(),
Home.ROUTE_NAME: (context) => Home(),
// /home/*
HomeCalendar.ROUTE_NAME: (BuildContext context) => HomeCalendar(refreshIndicatorKey: null),
HomeQuickAccess.ROUTE_NAME: (BuildContext context) => HomeQuickAccess(),
HomeCalendar.ROUTE_NAME: (context) => HomeCalendar(refreshIndicatorKey: null),
HomeQuickAccess.ROUTE_NAME: (context) => HomeQuickAccess(),
};
static Map<String, WidgetBuilder> get _settings => <String, WidgetBuilder> {
// /settings
Settings.ROUTE_NAME: (BuildContext context) => Settings(),
Settings.ROUTE_NAME: (context) => Settings(),
// /settings/*
SettingsIndexers.ROUTE_NAME: (BuildContext context) => SettingsIndexers(),
SettingsClients.ROUTE_NAME: (BuildContext context) => SettingsClients(),
SettingsGeneral.ROUTE_NAME: (BuildContext context) => SettingsGeneral(),
SettingsAutomation.ROUTE_NAME: (BuildContext context) => SettingsAutomation(),
// /settings/indexers/*
SettingsIndexersAdd.ROUTE_NAME: (BuildContext context) => SettingsIndexersAdd(),
SettingsIndexerEdit.ROUTE_NAME: (BuildContext context) => SettingsIndexerEdit(),
SettingsGeneral.ROUTE_NAME: (context) => SettingsGeneral(),
SettingsSystem.ROUTE_NAME: (context) => SettingsSystem(),
SettingsModules.ROUTE_NAME: (context) => SettingsModules(),
// /settings/modules/*
// Automation
SettingsModulesLidarr.ROUTE_NAME: (context) => SettingsModulesLidarr(),
SettingsModulesRadarr.ROUTE_NAME: (context) => SettingsModulesRadarr(),
SettingsModulesSonarr.ROUTE_NAME: (context) => SettingsModulesSonarr(),
// Clients
SettingsModulesNZBGet.ROUTE_NAME: (context) => SettingsModulesNZBGet(),
SettingsModulesSABnzbd.ROUTE_NAME: (context) => SettingsModulesSABnzbd(),
// General
SettingsModulesCalendar.ROUTE_NAME: (context) => SettingsModulesCalendar(),
SettingsModulesLunaSea.ROUTE_NAME: (context) => SettingsModulesLunaSea(),
SettingsModulesSearch.ROUTE_NAME: (context) => SettingsModulesSearch(),
// /settings/modules/indexers/*
SettingsModulesSearchAdd.ROUTE_NAME: (context) => SettingsModulesSearchAdd(),
SettingsModulesSearchEdit.ROUTE_NAME: (context) => SettingsModulesSearchEdit(),
// /settings/general/logs/*
SettingsGeneralLogsTypes.ROUTE_NAME: (BuildContext context) => SettingsGeneralLogsTypes(),
SettingsGeneralLogsView.ROUTE_NAME: (BuildContext context) => SettingsGeneralLogsView(),
SettingsGeneralLogsDetails.ROUTE_NAME: (BuildContext context) => SettingsGeneralLogsDetails(),
SettingsGeneralLogsTypes.ROUTE_NAME: (context) => SettingsGeneralLogsTypes(),
SettingsGeneralLogsView.ROUTE_NAME: (context) => SettingsGeneralLogsView(),
SettingsGeneralLogsDetails.ROUTE_NAME: (context) => SettingsGeneralLogsDetails(),
};
static Map<String, WidgetBuilder> get _search => <String, WidgetBuilder> {
// /search
Search.ROUTE_NAME: (BuildContext context) => Search(),
Search.ROUTE_NAME: (context) => Search(),
// /search/*
SearchSearch.ROUTE_NAME: (BuildContext context) => SearchSearch(),
SearchCategories.ROUTE_NAME: (BuildContext context) => SearchCategories(),
SearchSubcategories.ROUTE_NAME: (BuildContext context) => SearchSubcategories(),
SearchResults.ROUTE_NAME: (BuildContext context) => SearchResults(),
SearchDetails.ROUTE_NAME: (BuildContext context) => SearchDetails(),
SearchSearch.ROUTE_NAME: (context) => SearchSearch(),
SearchCategories.ROUTE_NAME: (context) => SearchCategories(),
SearchSubcategories.ROUTE_NAME: (context) => SearchSubcategories(),
SearchResults.ROUTE_NAME: (context) => SearchResults(),
SearchDetails.ROUTE_NAME: (context) => SearchDetails(),
};
static Map<String, WidgetBuilder> get _lidarr => <String, WidgetBuilder> {
// /lidarr
Lidarr.ROUTE_NAME: (BuildContext context) => Lidarr(),
Lidarr.ROUTE_NAME: (context) => Lidarr(),
// /lidarr/*
LidarrCatalogue.ROUTE_NAME: (BuildContext context) => LidarrCatalogue(refreshIndicatorKey: null, refreshAllPages: null),
LidarrMissing.ROUTE_NAME: (BuildContext context) => LidarrMissing(refreshIndicatorKey: null, refreshAllPages: null),
LidarrHistory.ROUTE_NAME: (BuildContext context) => LidarrHistory(refreshIndicatorKey: null, refreshAllPages: null),
LidarrCatalogue.ROUTE_NAME: (context) => LidarrCatalogue(refreshIndicatorKey: null, refreshAllPages: null),
LidarrMissing.ROUTE_NAME: (context) => LidarrMissing(refreshIndicatorKey: null, refreshAllPages: null),
LidarrHistory.ROUTE_NAME: (context) => LidarrHistory(refreshIndicatorKey: null, refreshAllPages: null),
// /lidarr/add/*
LidarrAddSearch.ROUTE_NAME: (BuildContext context) => LidarrAddSearch(),
LidarrAddDetails.ROUTE_NAME: (BuildContext context) => LidarrAddDetails(),
LidarrAddSearch.ROUTE_NAME: (context) => LidarrAddSearch(),
LidarrAddDetails.ROUTE_NAME: (context) => LidarrAddDetails(),
// /lidarr/edit/*
LidarrEditArtist.ROUTE_NAME: (BuildContext context) => LidarrEditArtist(),
LidarrEditArtist.ROUTE_NAME: (context) => LidarrEditArtist(),
// /lidarr/details/*
LidarrDetailsAlbum.ROUTE_NAME: (BuildContext context) => LidarrDetailsAlbum(),
LidarrDetailsArtist.ROUTE_NAME: (BuildContext context) => LidarrDetailsArtist(),
LidarrDetailsAlbum.ROUTE_NAME: (context) => LidarrDetailsAlbum(),
LidarrDetailsArtist.ROUTE_NAME: (context) => LidarrDetailsArtist(),
// /lidarr/search/*
LidarrSearchDetails.ROUTE_NAME: (BuildContext context) => LidarrSearchDetails(),
LidarrSearchResults.ROUTE_NAME: (BuildContext context) => LidarrSearchResults(),
LidarrSearchDetails.ROUTE_NAME: (context) => LidarrSearchDetails(),
LidarrSearchResults.ROUTE_NAME: (context) => LidarrSearchResults(),
};
static Map<String, WidgetBuilder> get _radarr => <String, WidgetBuilder> {
// /radarr
Radarr.ROUTE_NAME: (BuildContext context) => Radarr(),
Radarr.ROUTE_NAME: (context) => Radarr(),
// /radarr/*
RadarrCatalogue.ROUTE_NAME: (BuildContext context) => RadarrCatalogue(refreshIndicatorKey: null, refreshAllPages: null),
RadarrMissing.ROUTE_NAME: (BuildContext context) => RadarrMissing(refreshIndicatorKey: null, refreshAllPages: null),
RadarrUpcoming.ROUTE_NAME: (BuildContext context) => RadarrUpcoming(refreshIndicatorKey: null, refreshAllPages: null),
RadarrHistory.ROUTE_NAME: (BuildContext context) => RadarrHistory(refreshIndicatorKey: null, refreshAllPages: null),
RadarrCatalogue.ROUTE_NAME: (context) => RadarrCatalogue(refreshIndicatorKey: null, refreshAllPages: null),
RadarrMissing.ROUTE_NAME: (context) => RadarrMissing(refreshIndicatorKey: null, refreshAllPages: null),
RadarrUpcoming.ROUTE_NAME: (context) => RadarrUpcoming(refreshIndicatorKey: null, refreshAllPages: null),
RadarrHistory.ROUTE_NAME: (context) => RadarrHistory(refreshIndicatorKey: null, refreshAllPages: null),
// /radarr/add/*
RadarrAddSearch.ROUTE_NAME: (BuildContext context) => RadarrAddSearch(),
RadarrAddDetails.ROUTE_NAME: (BuildContext context) => RadarrAddDetails(),
RadarrAddSearch.ROUTE_NAME: (context) => RadarrAddSearch(),
RadarrAddDetails.ROUTE_NAME: (context) => RadarrAddDetails(),
// /radarr/details/*
RadarrDetailsMovie.ROUTE_NAME: (BuildContext context) => RadarrDetailsMovie(),
RadarrDetailsSearchResults.ROUTE_NAME: (BuildContext context) => RadarrDetailsSearchResults(),
RadarrDetailsMovie.ROUTE_NAME: (context) => RadarrDetailsMovie(),
RadarrDetailsSearchResults.ROUTE_NAME: (context) => RadarrDetailsSearchResults(),
// /radarr/edit/*
RadarrEditMovie.ROUTE_NAME: (BuildContext context) => RadarrEditMovie(),
RadarrEditMovie.ROUTE_NAME: (context) => RadarrEditMovie(),
};
static Map<String, WidgetBuilder> get _sonarr => <String, WidgetBuilder> {
// /sonarr
Sonarr.ROUTE_NAME: (BuildContext context) => Sonarr(),
Sonarr.ROUTE_NAME: (context) => Sonarr(),
// /sonarr/*
SonarrCatalogue.ROUTE_NAME: (BuildContext context) => SonarrCatalogue(refreshIndicatorKey: null, refreshAllPages: null),
SonarrMissing.ROUTE_NAME: (BuildContext context) => SonarrMissing(refreshIndicatorKey: null, refreshAllPages: null),
SonarrUpcoming.ROUTE_NAME: (BuildContext context) => SonarrUpcoming(refreshIndicatorKey: null, refreshAllPages: null),
SonarrHistory.ROUTE_NAME: (BuildContext context) => SonarrHistory(refreshIndicatorKey: null, refreshAllPages: null),
SonarrCatalogue.ROUTE_NAME: (context) => SonarrCatalogue(refreshIndicatorKey: null, refreshAllPages: null),
SonarrMissing.ROUTE_NAME: (context) => SonarrMissing(refreshIndicatorKey: null, refreshAllPages: null),
SonarrUpcoming.ROUTE_NAME: (context) => SonarrUpcoming(refreshIndicatorKey: null, refreshAllPages: null),
SonarrHistory.ROUTE_NAME: (context) => SonarrHistory(refreshIndicatorKey: null, refreshAllPages: null),
// /sonarr/add/*
SonarrAddSearch.ROUTE_NAME: (BuildContext context) => SonarrAddSearch(),
SonarrAddDetails.ROUTE_NAME: (BuildContext context) => SonarrAddDetails(),
SonarrAddSearch.ROUTE_NAME: (context) => SonarrAddSearch(),
SonarrAddDetails.ROUTE_NAME: (context) => SonarrAddDetails(),
// /sonarr/details/*
SonarrDetailsSeries.ROUTE_NAME: (BuildContext context) => SonarrDetailsSeries(),
SonarrDetailsSeason.ROUTE_NAME: (BuildContext context) => SonarrDetailsSeason(),
SonarrDetailsSeries.ROUTE_NAME: (context) => SonarrDetailsSeries(),
SonarrDetailsSeason.ROUTE_NAME: (context) => SonarrDetailsSeason(),
// /sonarr/edit/*
SonarrEditSeries.ROUTE_NAME: (BuildContext context) => SonarrEditSeries(),
SonarrEditSeries.ROUTE_NAME: (context) => SonarrEditSeries(),
// /sonarr/search/*
SonarrSearchDetails.ROUTE_NAME: (BuildContext context) => SonarrSearchDetails(),
SonarrSearchResults.ROUTE_NAME: (BuildContext context) => SonarrSearchResults(),
SonarrSearchDetails.ROUTE_NAME: (context) => SonarrSearchDetails(),
SonarrSearchResults.ROUTE_NAME: (context) => SonarrSearchResults(),
};
static Map<String, WidgetBuilder> get _nzbget => <String, WidgetBuilder> {
// /nzbget
NZBGet.ROUTE_NAME: (BuildContext context) => NZBGet(),
NZBGet.ROUTE_NAME: (context) => NZBGet(),
// /nzbget/*
NZBGetHistory.ROUTE_NAME: (BuildContext context) => NZBGetHistory(refreshIndicatorKey: null),
NZBGetQueue.ROUTE_NAME: (BuildContext context) => NZBGetQueue(refreshIndicatorKey: null),
NZBGetStatistics.ROUTE_NAME: (BuildContext context) => NZBGetStatistics(),
NZBGetHistory.ROUTE_NAME: (context) => NZBGetHistory(refreshIndicatorKey: null),
NZBGetQueue.ROUTE_NAME: (context) => NZBGetQueue(refreshIndicatorKey: null),
NZBGetStatistics.ROUTE_NAME: (context) => NZBGetStatistics(),
// /nzbget/history/*
NZBGetHistoryDetails.ROUTE_NAME: (BuildContext context) => NZBGetHistoryDetails(),
NZBGetHistoryDetails.ROUTE_NAME: (context) => NZBGetHistoryDetails(),
};
static Map<String, WidgetBuilder> get _sabnzbd => <String, WidgetBuilder> {
// /sabnzbd
SABnzbd.ROUTE_NAME: (BuildContext context) => SABnzbd(),
SABnzbd.ROUTE_NAME: (context) => SABnzbd(),
// /sabnzbd/*
SABnzbdHistory.ROUTE_NAME: (BuildContext context) => SABnzbdHistory(refreshIndicatorKey: null),
SABnzbdQueue.ROUTE_NAME: (BuildContext context) => SABnzbdQueue(refreshIndicatorKey: null),
SABnzbdStatistics.ROUTE_NAME: (BuildContext context) => SABnzbdStatistics(),
SABnzbdHistory.ROUTE_NAME: (context) => SABnzbdHistory(refreshIndicatorKey: null),
SABnzbdQueue.ROUTE_NAME: (context) => SABnzbdQueue(refreshIndicatorKey: null),
SABnzbdStatistics.ROUTE_NAME: (context) => SABnzbdStatistics(),
// /sabnzbd/history/*
SABnzbdHistoryDetails.ROUTE_NAME: (BuildContext context) => SABnzbdHistoryDetails(),
SABnzbdHistoryDetails.ROUTE_NAME: (context) => SABnzbdHistoryDetails(),
};
}

View File

@@ -1,16 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
class Themes {
Themes._();
static ThemeData getDefaultTheme() {
static ThemeData getDarkTheme() => LunaSeaDatabaseValue.THEME_AMOLED.data
? _pureBlackTheme()
: _midnightTheme();
static ThemeData _midnightTheme() {
const _textStyle = TextStyle(
color: Colors.white,
letterSpacing: Constants.UI_LETTER_SPACING,
);
return ThemeData(
brightness: Brightness.dark,
canvasColor: LSColors.primary,
primaryColor: LSColors.secondary,
accentColor: LSColors.accent,
@@ -19,6 +24,11 @@ class Themes {
splashColor: LSColors.splash,
dialogBackgroundColor: LSColors.secondary,
dividerColor: LSColors.accent.withAlpha(0),
dividerTheme: DividerThemeData(
color: LSColors.accent,
indent: 100.0,
endIndent: 100.0,
),
iconTheme: IconThemeData(
color: Colors.white,
),
@@ -38,6 +48,50 @@ class Themes {
subtitle2: _textStyle,
overline: _textStyle,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
);
}
static ThemeData _pureBlackTheme() {
const _textStyle = TextStyle(
color: Colors.white,
letterSpacing: Constants.UI_LETTER_SPACING,
);
return ThemeData(
brightness: Brightness.dark,
canvasColor: Colors.black,
primaryColor: Colors.black,
accentColor: LSColors.accent,
highlightColor: LSColors.secondary,
cardColor: Colors.black,
splashColor: LSColors.splash,
dialogBackgroundColor: Colors.black,
dividerColor: LSColors.accent.withAlpha(0),
dividerTheme: DividerThemeData(
color: LSColors.accent,
indent: 72.0,
endIndent: 72.0,
),
iconTheme: IconThemeData(
color: Colors.white,
),
unselectedWidgetColor: Colors.white,
textTheme: TextTheme(
bodyText1: _textStyle,
bodyText2: _textStyle,
headline1: _textStyle,
headline2: _textStyle,
headline3: _textStyle,
headline4: _textStyle,
headline5: _textStyle,
headline6: _textStyle,
button: _textStyle,
caption: _textStyle,
subtitle1: _textStyle,
subtitle2: _textStyle,
overline: _textStyle,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
);
}
}

View File

@@ -22,7 +22,7 @@ class LSCard extends StatelessWidget {
? EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0)
: margin,
color: color == null
? LSColors.secondary
? Theme.of(context).primaryColor
: color,
);
}

View File

@@ -9,7 +9,7 @@ Decoration LSCardBackground({ @required String uri, bool darken = false }) => Bo
uri,
useDiskCache: true,
loadFailedCallback: () {},
fallbackAssetImage: 'assets/images/secondary_color.png',
fallbackAssetImage: 'assets/images/colors/secondary.png',
retryLimit: 1,
),
colorFilter: ColorFilter.mode(LSColors.secondary.withOpacity(darken ? 0.10 : 0.20), BlendMode.dstATop),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/core/constants.dart';
class LSCardTile extends StatelessWidget {
@@ -15,7 +16,7 @@ class LSCardTile extends StatelessWidget {
LSCardTile({
@required this.title,
@required this.subtitle,
this.subtitle,
this.trailing,
this.leading,
this.onTap,

View File

@@ -18,7 +18,7 @@ class LSDialogSearch {
),
],
content: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: ['profile']),
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) => ValueListenableBuilder(
valueListenable: Database.profilesBox.listenable(),
builder: (context, profilesBox, widget) => LSDialog.content(
@@ -33,13 +33,13 @@ class LSDialogSearch {
height: 2,
color: LSColors.accent,
),
value: lunaBox.get('profile'),
value: lunaBox.get(LunaSeaDatabaseValue.ENABLED_PROFILE.key),
items: (profilesBox as Box).keys.map<DropdownMenuItem<String>>((dynamic value) => DropdownMenuItem(
value: value,
child: Text(value),
)).toList(),
onChanged: (value) {
lunaBox.put('profile', value);
lunaBox.put(LunaSeaDatabaseValue.ENABLED_PROFILE.key, value);
},
isExpanded: true,
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules.dart';
class LSDialogSettings {
LSDialogSettings._();
@@ -115,8 +116,9 @@ class LSDialogSettings {
textAlign: TextAlign.center,
text: TextSpan(
children: [
LSDialog.textSpanContent(text: 'The exported logs can be found in '),
LSDialog.bolded(title: '<On My Device>/LunaSea/FLogs', fontSize: 16.0),
if(Platform.isIOS || Platform.isAndroid) LSDialog.textSpanContent(text: 'The exported logs can be found in '),
if(Platform.isIOS) LSDialog.bolded(title: '<On My Device>/LunaSea/FLogs', fontSize: 16.0),
if(Platform.isAndroid) LSDialog.bolded(title: '<Storage>/Android/data/app.lunasea.lunasea/files/FLogs', fontSize: 16.0),
LSDialog.textSpanContent(text: '.'),
],
),
@@ -394,4 +396,124 @@ class LSDialogSettings {
);
return [_flag, _profile];
}
static Future<List> changeBrowser(BuildContext context) async {
//Returns
bool _flag = false;
LSBrowsers _browser;
//Setter
void _setValues(bool flag, LSBrowsers browser) {
_flag = flag;
_browser = browser;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Open Links In...'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
LSBrowsers.values.length,
(index) => ListTile(
leading: LSIcon(
icon: LSBrowsers.values[index].icon,
color: LSColors.list(index),
),
title: Text(
LSBrowsers.values[index].name,
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, LSBrowsers.values[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _browser];
}
static Future<List> homeChangeStartingDay(BuildContext context) async {
//Returns
bool _flag = false;
CalendarStartingDay _startingDate;
//Setter
void _setValues(bool flag, CalendarStartingDay startingDate) {
_flag = flag;
_startingDate = startingDate;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Starting Day of Week'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
CalendarStartingDay.values.length,
(index) => ListTile(
leading: LSIcon(
icon: CustomIcons.calendar,
color: LSColors.list(index),
),
title: Text(
CalendarStartingDay.values[index].name,
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, CalendarStartingDay.values[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _startingDate];
}
static Future<List> homeChangeStartingSize(BuildContext context) async {
//Returns
bool _flag = false;
CalendarStartingSize _startingSize;
//Setter
void _setValues(bool flag, CalendarStartingSize startingSize) {
_flag = flag;
_startingSize = startingSize;
Navigator.of(context).pop();
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: LSDialog.title(text: 'Starting Size'),
actions: <Widget>[
LSDialog.cancel(context, textColor: LSColors.accent),
],
content: LSDialog.content(
children: List.generate(
CalendarStartingSize.values.length,
(index) => ListTile(
leading: LSIcon(
icon: CalendarStartingSize.values[index].icon,
color: LSColors.list(index),
),
title: Text(
CalendarStartingSize.values[index].name,
style: TextStyle(color: Colors.white),
),
onTap: () => _setValues(true, CalendarStartingSize.values[index]),
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
),
),
),
contentPadding: EdgeInsets.only(left: 24.0, top: 20.0, right: 24.0),
),
);
return [_flag, _startingSize];
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
@@ -157,62 +159,58 @@ class LSDialogSystem {
},
),
],
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'Are you sure you want to backup your current configuration?\n',
content: LSDialog.content(
children: [
LSDialog.textContent(text: 'Are you sure you want to backup your current configuration?\n'),
LSDialog.textContent(text: 'All backups are encrypted before being exported to the filesystem, please enter an encryption key for the backup.\n'),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
if(Platform.isIOS || Platform.isAndroid) LSDialog.textSpanContent(text: 'The backups can be found in '),
if(Platform.isIOS) LSDialog.bolded(title: '<On My Device>/LunaSea/configurations', fontSize: 16.0),
if(Platform.isAndroid) LSDialog.bolded(title: '<Storage>/Android/data/app.lunasea.lunasea/files/configurations', fontSize: 16.0),
LSDialog.textSpanContent(text: '.'),
],
),
),
Form(
key: formKey,
child: TextFormField(
autofocus: true,
autocorrect: false,
obscureText: true,
controller: textController,
decoration: InputDecoration(
labelText: 'Encryption Key',
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
),
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
cursorColor: Color(Constants.ACCENT_COLOR),
validator: (value) {
if(value.length < 8) {
return 'Minimum of 8 characters';
}
return null;
},
),
Text(
'All backups are encrypted before being exported to the filesystem, please enter an encryption key for the backup.',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
Form(
key: formKey,
child: TextFormField(
autofocus: true,
autocorrect: false,
obscureText: true,
controller: textController,
decoration: InputDecoration(
labelText: 'Encryption Key',
labelStyle: TextStyle(
color: Colors.white54,
decoration: TextDecoration.none,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(Constants.ACCENT_COLOR),
),
),
),
style: TextStyle(
color: Colors.white,
),
cursorColor: Color(Constants.ACCENT_COLOR),
validator: (value) {
if(value.length < 8) {
return 'Minimum of 8 characters';
}
return null;
},
),
),
],
),
padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
),
],
),
);
}
@@ -291,7 +289,7 @@ class LSDialogSystem {
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Clear Configuration',
'Reset LunaSea',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@@ -311,7 +309,7 @@ class LSDialogSystem {
),
FlatButton(
child: Text(
'Clear',
'Reset',
style: TextStyle(
color: Colors.red,
),
@@ -324,7 +322,7 @@ class LSDialogSystem {
],
content: SingleChildScrollView(
child: Text(
'Are you sure you want to clear your configuration?\n\nYou will be starting from a clean slate, please ensure you backup your current configuration first!',
'Are you sure you want to reset LunaSea and clear your configuration?\n\nYou will be starting from a clean slate, please ensure you backup your current configuration first!',
style: TextStyle(
color: Colors.white,
),
@@ -338,70 +336,6 @@ class LSDialogSystem {
return [flag];
}
static Future<List<dynamic>> showDeleteConfigurationPrompt(BuildContext context, List<String> paths) async {
bool flag = false;
String path = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Delete Configuration',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: <Widget>[
FlatButton(
child: Text(
'Cancel',
style: TextStyle(
color: Color(Constants.ACCENT_COLOR),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
content: Container(
child: ListView.builder(
shrinkWrap: true,
itemCount: paths.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(
Icons.delete,
color: Constants.LIST_COLOR_ICONS[index%Constants.LIST_COLOR_ICONS.length],
),
title: Text(
paths[(paths.length-index-1)].substring(
paths[(paths.length-index-1)].indexOf('/configurations/')+16,
paths[(paths.length-index-1)].indexOf('.json')
).replaceAll('%', '\n'),
style: TextStyle(
color: Colors.white,
),
),
onTap: () async {
path = paths[paths.length-index-1];
flag = true;
Navigator.of(context).pop();
},
contentPadding: EdgeInsets.fromLTRB(32.0, 0.0, 0.0, 0.0),
);
},
),
width: 400,
),
contentPadding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
);
},
);
return [flag, path];
}
static Future<List<dynamic>> showEncryptionKeyPrompt(BuildContext context) async {
bool flag = false;
final formKey = GlobalKey<FormState>();

View File

@@ -1,21 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSDivider extends StatelessWidget {
final double padding;
final Color color;
LSDivider({
this.padding = 14.0,
this.color,
});
@override
Widget build(BuildContext context) => Divider(
color: color == null
? LSColors.accent
: color,
indent: padding,
endIndent: padding,
);
Widget build(BuildContext context) => Divider();
}

View File

@@ -6,25 +6,32 @@ class CustomIcons {
static const _kFontFam = 'CustomIcons';
static const _kFontPkg = null;
static const IconData arrow_right = const IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData calendar_missing = const IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData calendar = const IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData clients = const IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData documentation = const IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData github = const IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData history = const IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData home = const IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData layers = const IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData monitoring = const IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData movies = const IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData music = const IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData nzbget = const IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue = const IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData reddit = const IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData rss = const IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sabnzbd = const IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings = const IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData television = const IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData upcoming = const IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData user = const IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData arrow_right = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData calendar_missing = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData calendar = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData clients = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData documentation = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData github = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData history = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData home = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData layers = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData monitoring = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData movies = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData music = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData nzbget = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData reddit = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData rss = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sabnzbd = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings = IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData television = IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData upcoming = IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData user = IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData bravebrowser = IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microsoftedge = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData firefox = IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData safari = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chrome = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData code = IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData modules = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@@ -8,7 +8,6 @@ class LSTextInputBar extends StatefulWidget {
final IconData labelIcon;
final void Function(String, bool) onChanged;
final void Function(String) onSubmitted;
final Color color;
final EdgeInsets margin;
LSTextInputBar({
@@ -18,7 +17,6 @@ class LSTextInputBar extends StatefulWidget {
this.action = TextInputAction.search,
this.labelText = 'Search...',
this.labelIcon = Icons.search,
this.color,
this.margin = Constants.UI_CARD_MARGIN,
});
@@ -69,8 +67,6 @@ class _State extends State<LSTextInputBar> {
onChanged: (value) => widget.onChanged(value, false),
onSubmitted: widget.onSubmitted,
),
color: widget.color == null
? LSColors.secondary
: widget.color,
color: Theme.of(context).canvasColor,
);
}

View File

@@ -4,16 +4,20 @@ class LSListView extends StatelessWidget {
final List<Widget> children;
final bool padBottom;
final EdgeInsetsGeometry customPadding;
final ScrollController controller;
LSListView({
@required this.children,
this.padBottom = false,
this.controller,
this.customPadding = const EdgeInsets.symmetric(vertical: 8.0),
});
@override
Widget build(BuildContext context) => Scrollbar(
controller: controller,
child: ListView(
controller: controller,
children: children,
padding: padBottom
? EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 20.0)

View File

@@ -14,7 +14,7 @@ class LSNotEnabled extends StatelessWidget {
@override
Widget build(BuildContext context) => LSGenericMessage(
text: service == Constants.NO_SERVICES_ENABLED
? 'No Services Enabled'
? 'No Modules Enabled'
: '$service Is Not Enabled',
showButton: showButton,
buttonText: 'Return Home',

View File

@@ -14,6 +14,5 @@ Widget LSAppBar({
),
centerTitle: false,
elevation: 0,
backgroundColor: LSColors.secondary,
actions: actions,
);

View File

@@ -14,7 +14,6 @@ Widget LSAppBarTabs({
),
centerTitle: false,
elevation: 0,
backgroundColor: LSColors.secondary,
bottom: TabBar(
tabs: [
for(int i=0; i<tabTitles.length; i++)

View File

@@ -25,7 +25,7 @@ class LSBottomNavigationBar extends StatelessWidget {
iconSize: 24.0,
padding: EdgeInsets.fromLTRB(18.0, 5.0, 12.0, 5.0),
duration: Duration(milliseconds: Constants.UI_NAVIGATION_SPEED),
tabBackgroundColor: LSColors.primary,
tabBackgroundColor: Theme.of(context).canvasColor,
activeColor: LSColors.accent,
tabs: List.generate(
icons.length,
@@ -41,7 +41,8 @@ class LSBottomNavigationBar extends StatelessWidget {
),
),
decoration: BoxDecoration(
color: LSColors.secondary,
color: Theme.of(context).primaryColor,
//LSColors.secondary,
),
);
}

View File

@@ -10,12 +10,12 @@ class LSDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: ['profile']),
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) {
return ValueListenableBuilder(
valueListenable: Database.indexersBox.listenable(),
builder: (context, indexerBox, widget) {
ProfileHiveObject profile = Database.profilesBox.get(lunaBox.get('profile'));
ProfileHiveObject profile = Database.profilesBox.get(lunaBox.get(LunaSeaDatabaseValue.ENABLED_PROFILE.key));
return Drawer(
child: ListView(
children: _getDrawerEntries(context, profile, (ModuleFlags.SEARCH && (indexerBox as Box).length > 0)),
@@ -33,20 +33,20 @@ class LSDrawer extends StatelessWidget {
UserAccountsDrawerHeader(
accountName: LSTitle(text: 'LunaSea'),
accountEmail: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: ['profile']),
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.ENABLED_PROFILE.key]),
builder: (context, lunaBox, widget) => ValueListenableBuilder(
valueListenable: Database.profilesBox.listenable(),
builder: (context, profilesBox, widget) => Padding(
child: DropdownButton(
icon: LSIcon(icon: Icons.arrow_drop_down, color: Colors.white70),
underline: Container(),
value: lunaBox.get('profile'),
value: lunaBox.get(LunaSeaDatabaseValue.ENABLED_PROFILE.key),
items: (profilesBox as Box).keys.map<DropdownMenuItem<String>>((dynamic value) => DropdownMenuItem(
value: value,
child: Text(value),
)).toList(),
onChanged: (value) {
lunaBox.put('profile', value);
lunaBox.put(LunaSeaDatabaseValue.ENABLED_PROFILE.key, value);
},
isDense: true,
isExpanded: true,
@@ -76,7 +76,7 @@ class LSDrawer extends StatelessWidget {
route: '/settings',
justPush: true,
),
LSDivider(padding: 18.0),
LSDivider(),
if(showIndexerSearch) _buildEntry(
context: context,
icon: Icons.search,

View File

@@ -4,6 +4,7 @@ import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:lunasea/core.dart';
class LSSliverAppBar extends StatelessWidget {
final ScrollController _controller = ScrollController(initialScrollOffset: _EXPANDED_HEIGHT - 80.0);
final String title;
final String backgroundURI;
final List<Widget> actions;
@@ -16,41 +17,46 @@ class LSSliverAppBar extends StatelessWidget {
this.actions,
});
static const _EXPANDED_HEIGHT = 200.0;
@override
Widget build(BuildContext context) => NestedScrollView(
controller: _controller,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
sliver: SliverSafeArea(
top: false,
bottom: false,
sliver: SliverAppBar(
expandedHeight: 200.0,
expandedHeight: _EXPANDED_HEIGHT,
pinned: true,
elevation: Constants.UI_ELEVATION,
flexibleSpace: FlexibleSpaceBar(
title: Container(
child: Text(
title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(
letterSpacing: Constants.UI_LETTER_SPACING,
),
),
padding: EdgeInsets.symmetric(horizontal: 96.0),
title: Text(
title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
style: TextStyle(
letterSpacing: Constants.UI_LETTER_SPACING,
),
),
centerTitle: false,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: TransitionToImage(
image: AdvancedNetworkImage(
backgroundURI,
useDiskCache: true,
fallbackAssetImage: 'assets/images/secondary_color.png',
fallbackAssetImage: LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
retryLimit: 1,
),
fit: BoxFit.cover,
loadingWidget: Image.asset(
'assets/images/secondary_color.png',
LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
),
color: LSColors.secondary.withAlpha((255/1.5).floor()),
blendMode: BlendMode.darken,

View File

@@ -4,6 +4,7 @@ import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:lunasea/core.dart';
class LSSliverAppBarTabs extends StatelessWidget {
final ScrollController _controller = ScrollController(initialScrollOffset: _EXPANDED_HEIGHT - 80.0);
final String title;
final String backgroundURI;
final List<Widget> actions;
@@ -18,42 +19,46 @@ class LSSliverAppBarTabs extends StatelessWidget {
@required this.bottom,
});
static const _EXPANDED_HEIGHT = 200.0;
@override
Widget build(BuildContext context) => NestedScrollView(
controller: _controller,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
sliver: SliverSafeArea(
top: false,
bottom: false,
sliver: SliverAppBar(
expandedHeight: 200.0,
expandedHeight: _EXPANDED_HEIGHT,
pinned: true,
elevation: Constants.UI_ELEVATION,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 64.0),
title: Container(
child: Text(
title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(
letterSpacing: Constants.UI_LETTER_SPACING,
),
),
padding: EdgeInsets.symmetric(horizontal: 96.0),
title: Text(
title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
style: TextStyle(
letterSpacing: Constants.UI_LETTER_SPACING,
),
),
centerTitle: false,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: TransitionToImage(
image: AdvancedNetworkImage(
backgroundURI,
useDiskCache: true,
fallbackAssetImage: 'assets/images/secondary_color.png',
fallbackAssetImage: LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
retryLimit: 1,
),
fit: BoxFit.cover,
loadingWidget: Image.asset(
'assets/images/secondary_color.png',
LunaSeaDatabaseValue.THEME_AMOLED.data
? 'assets/images/colors/black.png'
: 'assets/images/colors/secondary.png',
),
color: LSColors.secondary.withAlpha((255/1.5).floor()),
blendMode: BlendMode.darken,

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
class LSRefreshIndicator extends StatefulWidget {
final Widget child;
@@ -21,7 +20,7 @@ class _State extends State<LSRefreshIndicator> {
Widget build(BuildContext context) {
return RefreshIndicator(
key: widget.refreshKey,
backgroundColor: LSColors.secondary,
backgroundColor: Theme.of(context).primaryColor,
onRefresh: widget.onRefresh,
child: widget.child,
);

View File

@@ -80,6 +80,6 @@ Future<void> LSSnackBar({
)
: null,
animationDuration: Duration(milliseconds: 375),
backgroundColor: LSColors.secondary,
backgroundColor: Theme.of(context).primaryColor,
)..show(context);
}

View File

@@ -30,7 +30,7 @@ class LSHeader extends StatelessWidget {
color: LSColors.accent,
),
),
padding: EdgeInsets.only(top: 4.0),
padding: EdgeInsets.only(top: 4.0, left: 1.0),
),
],
),

View File

@@ -1,24 +1,41 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
void main() async {
_init();
Logger.initialize();
await Database.initialize();
runZoned<Future<void>>(
runZonedGuarded<Future<void>>(
() async => runApp(_BIOS()),
onError: (Object error, StackTrace stack) => Logger.fatal(error, stack),
(Object error, StackTrace stack) => Logger.fatal(error, stack),
);
}
void _init() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Colors.black,
systemNavigationBarDividerColor: Colors.black,
statusBarColor: Colors.transparent,
));
}
class _BIOS extends StatelessWidget {
@override
Widget build(BuildContext context) => Providers.providers(
child: MaterialApp(
title: 'LunaSea',
debugShowCheckedModeBanner: false,
routes: Routes.getRoutes(),
theme: Themes.getDefaultTheme(),
child: ValueListenableBuilder(
valueListenable: Database.lunaSeaBox.listenable(keys: [LunaSeaDatabaseValue.THEME_AMOLED.key]),
builder: (context, box, _) {
return MaterialApp(
title: 'LunaSea',
debugShowCheckedModeBanner: false,
routes: Routes.getRoutes(),
darkTheme: Themes.getDarkTheme(),
theme: Themes.getDarkTheme(),
);
}
),
);
}

View File

@@ -1,4 +1,4 @@
export './home/api.dart';
export './home/core.dart';
export './home/routes.dart';
export './home/state.dart';
export './home/widgets.dart';

View File

@@ -1,156 +0,0 @@
import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
import '../../home.dart';
class CalendarAPI extends API {
final Map<String, dynamic> lidarr;
final Map<String, dynamic> radarr;
final Map<String, dynamic> sonarr;
CalendarAPI._internal({
@required this.lidarr,
@required this.radarr,
@required this.sonarr,
});
factory CalendarAPI.from(ProfileHiveObject profile) {
return CalendarAPI._internal(
lidarr: profile?.getLidarr(),
radarr: profile?.getRadarr(),
sonarr: profile?.getSonarr(),
);
}
void logError(String methodName, String text, Object error) => Logger.error('package:lunasea/core/api/calendar/api.dart', methodName, 'Home: $text', error, StackTrace.current);
void logWarning(String methodName, String text) => Logger.warning('package:lunasea/core/api/calendar/api.dart', methodName, 'Home: $text');
Future<bool> testConnection() async => true;
Future<Map<DateTime, List>> getUpcoming(DateTime today) async {
Map<DateTime, List> _upcoming = {};
await _getLidarrUpcoming(_upcoming, today);
await _getRadarrUpcoming(_upcoming, today);
await _getSonarrUpcoming(_upcoming, today);
return _upcoming.isEmpty
? await Future.delayed(Duration(milliseconds: 500), () => {})
: _upcoming;
}
Future<void> _getLidarrUpcoming(Map<DateTime, List> map, DateTime today, { int startOffset = 7, int endOffset = 60 }) async {
try {
if(ModuleFlags.AUTOMATION && ModuleFlags.LIDARR && lidarr['enabled']) {
String start = DateFormat('y-MM-dd').format(today.subtract(Duration(days: startOffset)));
String end = DateFormat('y-MM-dd').format(today.add(Duration(days: endOffset)));
String uri = '${lidarr['host']}/api/v1/calendar?apikey=${lidarr['key']}&start=$start&end=$end';
Dio _client = Dio();
if(!lidarr['strict_tls']) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
Response response = await _client.get(uri);
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['releaseDate'] ?? '')?.toLocal()?.lsDateTime_floor();
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarLidarrData(
id: entry['id'] ?? 0,
title: entry['artist']['artistName'] ?? 'Unknown Artist',
albumTitle: entry['title'] ?? 'Unknown Album Title',
artistId: entry['artist']['id'] ?? 0,
hasAllFiles: (entry['statistics'] != null ? entry['statistics']['percentOfTracks'] ?? 0 : 0) == 100,
));
map[date] = day;
}
}
}
}
} catch (error) {
logError('_getLidarrUpcoming', 'Failed to fetch Lidarr upcoming content', error);
return;
}
}
Future<void> _getRadarrUpcoming(Map<DateTime, List> map, DateTime today, { int startOffset = 7, int endOffset = 60 }) async {
try {
if(ModuleFlags.AUTOMATION && ModuleFlags.RADARR && radarr['enabled']) {
String start = DateFormat('y-MM-dd').format(today.subtract(Duration(days: startOffset)));
String end = DateFormat('y-MM-dd').format(today.add(Duration(days: endOffset)));
String uri = '${radarr['host']}/api/calendar?apikey=${radarr['key']}&start=$start&end=$end';
Dio _client = Dio();
if(!radarr['strict_tls']) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
Response response = await _client.get(uri);
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['physicalRelease'] ?? '')?.toLocal()?.lsDateTime_floor();
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarRadarrData(
id: entry['id'] ?? 0,
title: entry['title'] ?? 'Unknown Title',
hasFile: entry['hasFile'] ?? false,
fileQualityProfile: entry['hasFile'] ? entry['movieFile']['quality']['quality']['name'] : '',
year: entry['year'] ?? 0,
runtime: entry['runtime'] ?? 0,
));
map[date] = day;
}
}
}
}
} catch (error) {
logError('_getRadarrUpcoming', 'Failed to fetch Radarr upcoming content', error);
return;
}
}
Future<void> _getSonarrUpcoming(Map<DateTime, List> map, DateTime today, { int startOffset = 7, int endOffset = 60 }) async {
try {
if(ModuleFlags.AUTOMATION && ModuleFlags.SONARR && sonarr['enabled']) {
String start = DateFormat('y-MM-dd').format(today.subtract(Duration(days: startOffset)));
String end = DateFormat('y-MM-dd').format(today.add(Duration(days: endOffset)));
String uri = '${sonarr['host']}/api/calendar?apikey=${sonarr['key']}&start=$start&end=$end';
Dio _client = Dio();
if(!sonarr['strict_tls']) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
Response response = await _client.get(uri);
if(response.data.length > 0) {
for(var entry in response.data) {
DateTime date = DateTime.tryParse(entry['airDateUtc'] ?? '')?.toLocal()?.lsDateTime_floor();
if(date != null) {
List day = map[date] ?? [];
day.add(CalendarSonarrData(
id: entry['id'] ?? 0,
seriesID: entry['seriesId'] ?? 0,
title: entry['series']['title'] ?? 'Unknown Series',
episodeTitle: entry['title'] ?? 'Unknown Episode Title',
seasonNumber: entry['seasonNumber'] ?? -1,
episodeNumber: entry['episodeNumber'] ?? -1,
airTime: entry['airDateUtc'] ?? '',
hasFile: entry['hasFile'] ?? false,
fileQualityProfile: entry['hasFile'] ? entry['episodeFile']['quality']['quality']['name'] : '',
));
map[date] = day;
}
}
}
}
} catch (error) {
logError('_getSonarrUpcoming', 'Failed to fetch Sonarr upcoming content', error);
return;
}
}
}

View File

@@ -0,0 +1,3 @@
export './core/api.dart';
export './core/adapters.dart';
export './core/database.dart';

View File

@@ -0,0 +1,2 @@
export './adapters/calendar_starting_day.dart';
export './adapters/calendar_starting_size.dart';

View File

@@ -0,0 +1,50 @@
import 'package:lunasea/core/database/database.dart';
import 'package:table_calendar/table_calendar.dart';
part 'calendar_starting_day.g.dart';
@HiveType(typeId: 12, adapterName: 'CalendarStartingDayAdapter')
enum CalendarStartingDay {
@HiveField(0)
MONDAY,
@HiveField(1)
TUESDAY,
@HiveField(2)
WEDNESDAY,
@HiveField(3)
THURSDAY,
@HiveField(4)
FRIDAY,
@HiveField(5)
SATURDAY,
@HiveField(6)
SUNDAY,
}
extension CalendarStartingDayExtension on CalendarStartingDay {
StartingDayOfWeek get data {
switch(this) {
case CalendarStartingDay.MONDAY: return StartingDayOfWeek.monday;
case CalendarStartingDay.TUESDAY: return StartingDayOfWeek.tuesday;
case CalendarStartingDay.WEDNESDAY: return StartingDayOfWeek.wednesday;
case CalendarStartingDay.THURSDAY: return StartingDayOfWeek.thursday;
case CalendarStartingDay.FRIDAY: return StartingDayOfWeek.friday;
case CalendarStartingDay.SATURDAY: return StartingDayOfWeek.saturday;
case CalendarStartingDay.SUNDAY: return StartingDayOfWeek.sunday;
default: return StartingDayOfWeek.monday;
}
}
String get name {
switch(this) {
case CalendarStartingDay.MONDAY: return 'Monday';
case CalendarStartingDay.TUESDAY: return 'Tuesday';
case CalendarStartingDay.WEDNESDAY: return 'Wednesday';
case CalendarStartingDay.THURSDAY: return 'Thursday';
case CalendarStartingDay.FRIDAY: return 'Friday';
case CalendarStartingDay.SATURDAY: return 'Saturday';
case CalendarStartingDay.SUNDAY: return 'Sunday';
default: return 'Unknown Starting Day';
}
}
}

View File

@@ -0,0 +1,61 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'calendar_starting_day.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CalendarStartingDayAdapter extends TypeAdapter<CalendarStartingDay> {
@override
final typeId = 12;
@override
CalendarStartingDay read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return CalendarStartingDay.MONDAY;
case 1:
return CalendarStartingDay.TUESDAY;
case 2:
return CalendarStartingDay.WEDNESDAY;
case 3:
return CalendarStartingDay.THURSDAY;
case 4:
return CalendarStartingDay.FRIDAY;
case 5:
return CalendarStartingDay.SATURDAY;
case 6:
return CalendarStartingDay.SUNDAY;
default:
return null;
}
}
@override
void write(BinaryWriter writer, CalendarStartingDay obj) {
switch (obj) {
case CalendarStartingDay.MONDAY:
writer.writeByte(0);
break;
case CalendarStartingDay.TUESDAY:
writer.writeByte(1);
break;
case CalendarStartingDay.WEDNESDAY:
writer.writeByte(2);
break;
case CalendarStartingDay.THURSDAY:
writer.writeByte(3);
break;
case CalendarStartingDay.FRIDAY:
writer.writeByte(4);
break;
case CalendarStartingDay.SATURDAY:
writer.writeByte(5);
break;
case CalendarStartingDay.SUNDAY:
writer.writeByte(6);
break;
}
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core/database/database.dart';
import 'package:table_calendar/table_calendar.dart';
part 'calendar_starting_size.g.dart';
@HiveType(typeId: 13, adapterName: 'CalendarStartingSizeAdapter')
enum CalendarStartingSize {
@HiveField(0)
ONE_WEEK,
@HiveField(1)
TWO_WEEKS,
@HiveField(2)
ONE_MONTH,
}
extension CalendarStartingSizeExtension on CalendarStartingSize {
CalendarFormat get data {
switch(this) {
case CalendarStartingSize.TWO_WEEKS: return CalendarFormat.twoWeeks;
case CalendarStartingSize.ONE_MONTH: return CalendarFormat.month;
case CalendarStartingSize.ONE_WEEK:
default: return CalendarFormat.week;
}
}
String get name {
switch(this) {
case CalendarStartingSize.TWO_WEEKS: return 'Two Weeks';
case CalendarStartingSize.ONE_MONTH: return 'One Month';
case CalendarStartingSize.ONE_WEEK:
default: return 'One Week';
}
}
IconData get icon {
switch(this) {
case CalendarStartingSize.TWO_WEEKS: return Icons.photo_size_select_large;
case CalendarStartingSize.ONE_MONTH: return Icons.photo_size_select_actual;
case CalendarStartingSize.ONE_WEEK:
default: return Icons.photo_size_select_small;
}
}
}

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