something
43
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
45
.metadata
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: android
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: web
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
25
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "whispering_pages",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whispering_pages (profile mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whispering_pages (release mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
.vscode/settings.json
vendored
|
|
@ -3,5 +3,10 @@
|
||||||
"activityBar.background": "#5A1021",
|
"activityBar.background": "#5A1021",
|
||||||
"titleBar.activeBackground": "#7E162E",
|
"titleBar.activeBackground": "#7E162E",
|
||||||
"titleBar.activeForeground": "#FEFBFC"
|
"titleBar.activeForeground": "#FEFBFC"
|
||||||
}
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"**/*.freezed.dart": true,
|
||||||
|
"**/*.g.dart": true
|
||||||
|
},
|
||||||
|
"cSpell.words": ["Autovalidate", "mocktail", "riverpod", "shelfsdk"]
|
||||||
}
|
}
|
||||||
37
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"icon": { "id": "eye-watch", "color": "terminal.ansiYellow" },
|
||||||
|
"label": "build_runner watch",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "dart run build_runner watch",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"detail": "Running build_runner watch for code generation",
|
||||||
|
"presentation": {
|
||||||
|
"revealProblems": "onProblem",
|
||||||
|
"reveal": "silent",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
|
"runOptions": {
|
||||||
|
"instanceLimit": 1,
|
||||||
|
"runOn": "folderOpen",
|
||||||
|
"reevaluateOnRerun": true
|
||||||
|
},
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "dart",
|
||||||
|
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# whispering_pages
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
33
analysis_options.yaml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
require_trailing_commas: true
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
invalid_annotation_target: ignore
|
||||||
|
plugins:
|
||||||
|
- custom_lint
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
13
android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
67
android/app/build.gradle
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.example.whispering_pages"
|
||||||
|
compileSdk flutter.compileSdkVersion
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "com.example.whispering_pages"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
minSdkVersion 23
|
||||||
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
44
android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="whispering_pages"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility?hl=en and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.example.whispering_pages
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?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="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?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="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine 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">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine 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.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
18
android/build.gradle
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
3
android/gradle.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
org.gradle.jvmargs=-Xmx4G
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||||
26
android/settings.gradle
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
|
id "com.android.application" version "7.3.0" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
1172
assets/animations/Animation - 1714930099660.json
Normal file
143
lib/api/api_provider.dart
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
// provider to provide the api instance
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
import 'package:whispering_pages/db/cache_manager.dart';
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
|
||||||
|
part 'api_provider.g.dart';
|
||||||
|
|
||||||
|
Uri makeBaseUrl(String address) {
|
||||||
|
if (!address.startsWith('http') && !address.startsWith('https')) {
|
||||||
|
address = 'https://$address';
|
||||||
|
}
|
||||||
|
if (!Uri.parse(address).isAbsolute) {
|
||||||
|
throw ArgumentError.value(address, 'address', 'Invalid address');
|
||||||
|
}
|
||||||
|
return Uri.parse(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
@riverpod
|
||||||
|
AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
|
||||||
|
// try to get the base url from app settings
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
baseUrl ??= apiSettings.activeServer?.serverUrl;
|
||||||
|
if (baseUrl == null) {
|
||||||
|
throw ArgumentError.notNull('baseUrl');
|
||||||
|
}
|
||||||
|
return AudiobookshelfApi(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the api instance for the authenticated user
|
||||||
|
///
|
||||||
|
/// if the user is not authenticated throw an error
|
||||||
|
@riverpod
|
||||||
|
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
final user = apiSettings.activeUser;
|
||||||
|
if (user == null) {
|
||||||
|
throw StateError('No active user');
|
||||||
|
}
|
||||||
|
return AudiobookshelfApi(
|
||||||
|
baseUrl: Uri.https(user.server.serverUrl.toString()),
|
||||||
|
token: user.authToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
@riverpod
|
||||||
|
FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
|
||||||
|
// return (await ref.watch(audiobookshelfApiProvider).server.ping()) ?? false;
|
||||||
|
// if address not starts with http or https, add https
|
||||||
|
|
||||||
|
// !remove this line
|
||||||
|
// return true;
|
||||||
|
|
||||||
|
if (address.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!address.startsWith('http') && !address.startsWith('https')) {
|
||||||
|
address = 'https://$address';
|
||||||
|
}
|
||||||
|
|
||||||
|
// check url is valid
|
||||||
|
if (!Uri.parse(address).isAbsolute) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await AudiobookshelfApi(baseUrl: Uri.parse(address)).server.ping() ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch the personalized view
|
||||||
|
@riverpod
|
||||||
|
class PersonalizedView extends _$PersonalizedView {
|
||||||
|
@override
|
||||||
|
Stream<List<Shelf>> build() async* {
|
||||||
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
final user = apiSettings.activeUser;
|
||||||
|
if (apiSettings.activeLibraryId == null) {
|
||||||
|
// set it to default user library by logging in and getting the library id
|
||||||
|
final login =
|
||||||
|
await api.login(username: user!.username!, password: user.password!);
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// try to find in cache
|
||||||
|
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
|
||||||
|
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}';
|
||||||
|
final cachedRes = await apiResponseCacheManager.getFileFromCache(
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
if (cachedRes != null) {
|
||||||
|
final resJson = jsonDecode(await cachedRes.file.readAsString()) as List;
|
||||||
|
final res = [
|
||||||
|
for (final item in resJson)
|
||||||
|
Shelf.fromJson(item as Map<String, dynamic>),
|
||||||
|
];
|
||||||
|
debugPrint('reading from cache: $cachedRes');
|
||||||
|
yield res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ! exagerated delay
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
final res = await api.libraries
|
||||||
|
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
|
||||||
|
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
|
||||||
|
// save to cache
|
||||||
|
final newFile = await apiResponseCacheManager.putFile(
|
||||||
|
key,
|
||||||
|
utf8.encode(jsonEncode(res)),
|
||||||
|
fileExtension: 'json',
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
debugPrint('writing to cache: $newFile');
|
||||||
|
yield res!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// method to force refresh the view and ignore the cache
|
||||||
|
Future<void> forceRefresh() async {
|
||||||
|
// clear the cache
|
||||||
|
return apiResponseCacheManager.emptyCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch continue listening audiobooks
|
||||||
|
@riverpod
|
||||||
|
FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
||||||
|
FetchContinueListeningRef ref,
|
||||||
|
) async {
|
||||||
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
|
final res = await api.me.getSessions();
|
||||||
|
// debugPrint(
|
||||||
|
// 'fetchContinueListening: ${res.sessions.map((e) => e.libraryItemId).toSet()}',
|
||||||
|
// );
|
||||||
|
return res!;
|
||||||
|
}
|
||||||
370
lib/api/api_provider.g.dart
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'api_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$audiobookshelfApiHash() => r'5eb091c6b18c0bf5a0eec079fdb872a84c4f00d9';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
///
|
||||||
|
/// Copied from [audiobookshelfApi].
|
||||||
|
@ProviderFor(audiobookshelfApi)
|
||||||
|
const audiobookshelfApiProvider = AudiobookshelfApiFamily();
|
||||||
|
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
///
|
||||||
|
/// Copied from [audiobookshelfApi].
|
||||||
|
class AudiobookshelfApiFamily extends Family<AudiobookshelfApi> {
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
///
|
||||||
|
/// Copied from [audiobookshelfApi].
|
||||||
|
const AudiobookshelfApiFamily();
|
||||||
|
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
///
|
||||||
|
/// Copied from [audiobookshelfApi].
|
||||||
|
AudiobookshelfApiProvider call(
|
||||||
|
Uri? baseUrl,
|
||||||
|
) {
|
||||||
|
return AudiobookshelfApiProvider(
|
||||||
|
baseUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AudiobookshelfApiProvider getProviderOverride(
|
||||||
|
covariant AudiobookshelfApiProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.baseUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'audiobookshelfApiProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
///
|
||||||
|
/// Copied from [audiobookshelfApi].
|
||||||
|
class AudiobookshelfApiProvider extends AutoDisposeProvider<AudiobookshelfApi> {
|
||||||
|
/// get the api instance for the given base url
|
||||||
|
///
|
||||||
|
/// Copied from [audiobookshelfApi].
|
||||||
|
AudiobookshelfApiProvider(
|
||||||
|
Uri? baseUrl,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => audiobookshelfApi(
|
||||||
|
ref as AudiobookshelfApiRef,
|
||||||
|
baseUrl,
|
||||||
|
),
|
||||||
|
from: audiobookshelfApiProvider,
|
||||||
|
name: r'audiobookshelfApiProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$audiobookshelfApiHash,
|
||||||
|
dependencies: AudiobookshelfApiFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
AudiobookshelfApiFamily._allTransitiveDependencies,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
AudiobookshelfApiProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.baseUrl,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final Uri? baseUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
AudiobookshelfApi Function(AudiobookshelfApiRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: AudiobookshelfApiProvider._internal(
|
||||||
|
(ref) => create(ref as AudiobookshelfApiRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeProviderElement<AudiobookshelfApi> createElement() {
|
||||||
|
return _AudiobookshelfApiProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is AudiobookshelfApiProvider && other.baseUrl == baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, baseUrl.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> {
|
||||||
|
/// The parameter `baseUrl` of this provider.
|
||||||
|
Uri? get baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudiobookshelfApiProviderElement
|
||||||
|
extends AutoDisposeProviderElement<AudiobookshelfApi>
|
||||||
|
with AudiobookshelfApiRef {
|
||||||
|
_AudiobookshelfApiProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$authenticatedApiHash() => r'62213d5d0268eeaa2a16211cd60b1b6f0d19dd40';
|
||||||
|
|
||||||
|
/// get the api instance for the authenticated user
|
||||||
|
///
|
||||||
|
/// if the user is not authenticated throw an error
|
||||||
|
///
|
||||||
|
/// Copied from [authenticatedApi].
|
||||||
|
@ProviderFor(authenticatedApi)
|
||||||
|
final authenticatedApiProvider =
|
||||||
|
AutoDisposeProvider<AudiobookshelfApi>.internal(
|
||||||
|
authenticatedApi,
|
||||||
|
name: r'authenticatedApiProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authenticatedApiHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef AuthenticatedApiRef = AutoDisposeProviderRef<AudiobookshelfApi>;
|
||||||
|
String _$isServerAliveHash() => r'f839350795fbdeb0ca1d5f0c84a9065cac4dd40a';
|
||||||
|
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
///
|
||||||
|
/// Copied from [isServerAlive].
|
||||||
|
@ProviderFor(isServerAlive)
|
||||||
|
const isServerAliveProvider = IsServerAliveFamily();
|
||||||
|
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
///
|
||||||
|
/// Copied from [isServerAlive].
|
||||||
|
class IsServerAliveFamily extends Family<AsyncValue<bool>> {
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
///
|
||||||
|
/// Copied from [isServerAlive].
|
||||||
|
const IsServerAliveFamily();
|
||||||
|
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
///
|
||||||
|
/// Copied from [isServerAlive].
|
||||||
|
IsServerAliveProvider call(
|
||||||
|
String address,
|
||||||
|
) {
|
||||||
|
return IsServerAliveProvider(
|
||||||
|
address,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
IsServerAliveProvider getProviderOverride(
|
||||||
|
covariant IsServerAliveProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.address,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'isServerAliveProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
///
|
||||||
|
/// Copied from [isServerAlive].
|
||||||
|
class IsServerAliveProvider extends AutoDisposeFutureProvider<bool> {
|
||||||
|
/// ping the server to check if it is reachable
|
||||||
|
///
|
||||||
|
/// Copied from [isServerAlive].
|
||||||
|
IsServerAliveProvider(
|
||||||
|
String address,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => isServerAlive(
|
||||||
|
ref as IsServerAliveRef,
|
||||||
|
address,
|
||||||
|
),
|
||||||
|
from: isServerAliveProvider,
|
||||||
|
name: r'isServerAliveProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$isServerAliveHash,
|
||||||
|
dependencies: IsServerAliveFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
IsServerAliveFamily._allTransitiveDependencies,
|
||||||
|
address: address,
|
||||||
|
);
|
||||||
|
|
||||||
|
IsServerAliveProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.address,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String address;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<bool> Function(IsServerAliveRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: IsServerAliveProvider._internal(
|
||||||
|
(ref) => create(ref as IsServerAliveRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
address: address,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<bool> createElement() {
|
||||||
|
return _IsServerAliveProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is IsServerAliveProvider && other.address == address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, address.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> {
|
||||||
|
/// The parameter `address` of this provider.
|
||||||
|
String get address;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsServerAliveProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<bool> with IsServerAliveRef {
|
||||||
|
_IsServerAliveProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get address => (origin as IsServerAliveProvider).address;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$fetchContinueListeningHash() =>
|
||||||
|
r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d';
|
||||||
|
|
||||||
|
/// fetch continue listening audiobooks
|
||||||
|
///
|
||||||
|
/// Copied from [fetchContinueListening].
|
||||||
|
@ProviderFor(fetchContinueListening)
|
||||||
|
final fetchContinueListeningProvider =
|
||||||
|
AutoDisposeFutureProvider<GetUserSessionsResponse>.internal(
|
||||||
|
fetchContinueListening,
|
||||||
|
name: r'fetchContinueListeningProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$fetchContinueListeningHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef FetchContinueListeningRef
|
||||||
|
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
|
||||||
|
String _$personalizedViewHash() => r'52a89c46ce668238ca11b5394fd1d14c910947f5';
|
||||||
|
|
||||||
|
/// fetch the personalized view
|
||||||
|
///
|
||||||
|
/// Copied from [PersonalizedView].
|
||||||
|
@ProviderFor(PersonalizedView)
|
||||||
|
final personalizedViewProvider =
|
||||||
|
AutoDisposeStreamNotifierProvider<PersonalizedView, List<Shelf>>.internal(
|
||||||
|
PersonalizedView.new,
|
||||||
|
name: r'personalizedViewProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$personalizedViewHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
79
lib/api/authenticated_user_provider.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:whispering_pages/api/server_provider.dart'
|
||||||
|
show audiobookShelfServerProvider;
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/authenticated_user.dart' as model;
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/db/storage.dart';
|
||||||
|
|
||||||
|
part 'authenticated_user_provider.g.dart';
|
||||||
|
|
||||||
|
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
||||||
|
|
||||||
|
/// provides with a set of authenticated users
|
||||||
|
@riverpod
|
||||||
|
class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
|
@override
|
||||||
|
Set<model.AuthenticatedUser> build() {
|
||||||
|
ref.listenSelf((_, __) {
|
||||||
|
writeStateToBox();
|
||||||
|
});
|
||||||
|
// get the app settings
|
||||||
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
|
|
||||||
|
final availUsers = readFromBoxOrCreate();
|
||||||
|
if (apiSettings.activeUser != null) {
|
||||||
|
availUsers.add(apiSettings.activeUser!);
|
||||||
|
}
|
||||||
|
return availUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
|
||||||
|
if (_box.isNotEmpty) {
|
||||||
|
final foundData = _box.getRange(0, _box.length);
|
||||||
|
debugPrint('found users in box: $foundData');
|
||||||
|
return foundData.toSet();
|
||||||
|
} else {
|
||||||
|
debugPrint('no settings found in box');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeStateToBox() {
|
||||||
|
_box.clear();
|
||||||
|
if (state.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_box.addAll(state);
|
||||||
|
debugPrint('writing state to box: $state');
|
||||||
|
}
|
||||||
|
|
||||||
|
void addUser(model.AuthenticatedUser user) {
|
||||||
|
state = state..add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeUsersOfServer(AudiobookShelfServer registeredServer) {
|
||||||
|
state = state.where((user) => user.server != registeredServer).toSet();
|
||||||
|
// remove the server from the server provider
|
||||||
|
final serverProvider = ref.read(audiobookShelfServerProvider);
|
||||||
|
if (serverProvider.contains(registeredServer)) {
|
||||||
|
ref
|
||||||
|
.read(audiobookShelfServerProvider.notifier)
|
||||||
|
.removeServer(registeredServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeUser(model.AuthenticatedUser user) {
|
||||||
|
state = state.where((u) => u != user).toSet();
|
||||||
|
// also remove the user from the active user
|
||||||
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
|
if (apiSettings.activeUser == user) {
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeUser: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/api/authenticated_user_provider.g.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'authenticated_user_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$authenticatedUserHash() => r'5702fb6ab1e83129d57c89ef02a65c5910f2a076';
|
||||||
|
|
||||||
|
/// provides with a set of authenticated users
|
||||||
|
///
|
||||||
|
/// Copied from [AuthenticatedUser].
|
||||||
|
@ProviderFor(AuthenticatedUser)
|
||||||
|
final authenticatedUserProvider = AutoDisposeNotifierProvider<AuthenticatedUser,
|
||||||
|
Set<model.AuthenticatedUser>>.internal(
|
||||||
|
AuthenticatedUser.new,
|
||||||
|
name: r'authenticatedUserProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authenticatedUserHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AuthenticatedUser = AutoDisposeNotifier<Set<model.AuthenticatedUser>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
55
lib/api/image_provider.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
import 'package:whispering_pages/api/api_provider.dart';
|
||||||
|
import 'package:whispering_pages/db/cache_manager.dart';
|
||||||
|
|
||||||
|
/// provides cover images for the audiobooks
|
||||||
|
///
|
||||||
|
/// is a stream provider that provides cover images first from the cache then from the server
|
||||||
|
/// if the image is not found in the cache, it will be fetched from the server and saved to the cache
|
||||||
|
/// if the image is not found in the server it will throw an error
|
||||||
|
|
||||||
|
part 'image_provider.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class CoverImage extends _$CoverImage {
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> build(LibraryItem libraryItem) async* {
|
||||||
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
|
|
||||||
|
// try to get the image from the cache
|
||||||
|
final file = await imageCacheManager.getFileFromCache(libraryItem.id);
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
|
// if the image is in the cache, yield it
|
||||||
|
yield await file.file.readAsBytes();
|
||||||
|
// return if no need to fetch from the server
|
||||||
|
if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'cover image stale for ${libraryItem.id}, fetching from the server',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the image is in the cache
|
||||||
|
final coverImage = await api.items.getCover(
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
parameters: const GetImageReqParams(width: 500),
|
||||||
|
);
|
||||||
|
// save the image to the cache
|
||||||
|
final newFile = await imageCacheManager.putFile(
|
||||||
|
libraryItem.id,
|
||||||
|
coverImage ?? Uint8List(0),
|
||||||
|
key: libraryItem.id,
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}',
|
||||||
|
);
|
||||||
|
yield coverImage ?? Uint8List(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
174
lib/api/image_provider.g.dart
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'image_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$coverImageHash() => r'34c6aaf6831fea198984d22ecdf2c5b74e110891';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$CoverImage
|
||||||
|
extends BuildlessAutoDisposeStreamNotifier<Uint8List> {
|
||||||
|
late final LibraryItem libraryItem;
|
||||||
|
|
||||||
|
Stream<Uint8List> build(
|
||||||
|
LibraryItem libraryItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [CoverImage].
|
||||||
|
@ProviderFor(CoverImage)
|
||||||
|
const coverImageProvider = CoverImageFamily();
|
||||||
|
|
||||||
|
/// See also [CoverImage].
|
||||||
|
class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
|
||||||
|
/// See also [CoverImage].
|
||||||
|
const CoverImageFamily();
|
||||||
|
|
||||||
|
/// See also [CoverImage].
|
||||||
|
CoverImageProvider call(
|
||||||
|
LibraryItem libraryItem,
|
||||||
|
) {
|
||||||
|
return CoverImageProvider(
|
||||||
|
libraryItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
CoverImageProvider getProviderOverride(
|
||||||
|
covariant CoverImageProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.libraryItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'coverImageProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [CoverImage].
|
||||||
|
class CoverImageProvider
|
||||||
|
extends AutoDisposeStreamNotifierProviderImpl<CoverImage, Uint8List> {
|
||||||
|
/// See also [CoverImage].
|
||||||
|
CoverImageProvider(
|
||||||
|
LibraryItem libraryItem,
|
||||||
|
) : this._internal(
|
||||||
|
() => CoverImage()..libraryItem = libraryItem,
|
||||||
|
from: coverImageProvider,
|
||||||
|
name: r'coverImageProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$coverImageHash,
|
||||||
|
dependencies: CoverImageFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
CoverImageFamily._allTransitiveDependencies,
|
||||||
|
libraryItem: libraryItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
CoverImageProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.libraryItem,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final LibraryItem libraryItem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> runNotifierBuild(
|
||||||
|
covariant CoverImage notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
libraryItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(CoverImage Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: CoverImageProvider._internal(
|
||||||
|
() => create()..libraryItem = libraryItem,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
libraryItem: libraryItem,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
|
||||||
|
createElement() {
|
||||||
|
return _CoverImageProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is CoverImageProvider && other.libraryItem == libraryItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, libraryItem.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin CoverImageRef on AutoDisposeStreamNotifierProviderRef<Uint8List> {
|
||||||
|
/// The parameter `libraryItem` of this provider.
|
||||||
|
LibraryItem get libraryItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CoverImageProviderElement
|
||||||
|
extends AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
|
||||||
|
with CoverImageRef {
|
||||||
|
_CoverImageProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
LibraryItem get libraryItem => (origin as CoverImageProvider).libraryItem;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
102
lib/api/server_provider.dart
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:whispering_pages/api/authenticated_user_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/db/storage.dart';
|
||||||
|
|
||||||
|
part 'server_provider.g.dart';
|
||||||
|
|
||||||
|
final _box = AvailableHiveBoxes.serverBox;
|
||||||
|
|
||||||
|
class ServerAlreadyExistsException implements Exception {
|
||||||
|
final model.AudiobookShelfServer server;
|
||||||
|
|
||||||
|
ServerAlreadyExistsException(this.server);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Server $server already exists';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// provides with a set of servers added by the user
|
||||||
|
@riverpod
|
||||||
|
class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
|
@override
|
||||||
|
Set<model.AudiobookShelfServer> build() {
|
||||||
|
ref.listenSelf((_, __) {
|
||||||
|
writeStateToBox();
|
||||||
|
});
|
||||||
|
// get the app settings
|
||||||
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
|
// is default server is present, add it to the set
|
||||||
|
final availableServers = readFromBoxOrCreate();
|
||||||
|
if (apiSettings.activeServer != null) {
|
||||||
|
availableServers.add(apiSettings.activeServer!);
|
||||||
|
}
|
||||||
|
// also add server of the user
|
||||||
|
if (apiSettings.activeUser != null) {
|
||||||
|
availableServers.add(apiSettings.activeUser!.server);
|
||||||
|
}
|
||||||
|
return availableServers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
|
||||||
|
if (_box.isNotEmpty) {
|
||||||
|
final foundServers = _box.getRange(0, _box.length);
|
||||||
|
debugPrint('found servers in box: $foundServers');
|
||||||
|
return foundServers.whereNotNull().toSet();
|
||||||
|
} else {
|
||||||
|
debugPrint('no settings found in box');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeStateToBox() {
|
||||||
|
_box.clear();
|
||||||
|
if (state.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_box.addAll(state);
|
||||||
|
debugPrint('writing state to box: $state');
|
||||||
|
}
|
||||||
|
|
||||||
|
void addServer(model.AudiobookShelfServer server) {
|
||||||
|
if (state.contains(server)) {
|
||||||
|
throw ServerAlreadyExistsException(server);
|
||||||
|
}
|
||||||
|
state = {...state, server};
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeServer(model.AudiobookShelfServer server,
|
||||||
|
{
|
||||||
|
bool removeUsers = false,
|
||||||
|
}) {
|
||||||
|
state = state.where((s) => s != server).toSet();
|
||||||
|
// remove the server from the active server
|
||||||
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
|
if (apiSettings.activeServer == server) {
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeServer: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// remove the users of this server
|
||||||
|
if (removeUsers) {
|
||||||
|
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ? this doesn't seem to be useful
|
||||||
|
void updateServer(model.AudiobookShelfServer newServer) {
|
||||||
|
state = state
|
||||||
|
.map(
|
||||||
|
(existingServer) =>
|
||||||
|
existingServer == newServer ? newServer : existingServer,
|
||||||
|
)
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/api/server_provider.g.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'server_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$audiobookShelfServerHash() =>
|
||||||
|
r'f0d645bb42233c59886bc43fdc473897484ceca1';
|
||||||
|
|
||||||
|
/// provides with a set of servers added by the user
|
||||||
|
///
|
||||||
|
/// Copied from [AudiobookShelfServer].
|
||||||
|
@ProviderFor(AudiobookShelfServer)
|
||||||
|
final audiobookShelfServerProvider = AutoDisposeNotifierProvider<
|
||||||
|
AudiobookShelfServer, Set<model.AudiobookShelfServer>>.internal(
|
||||||
|
AudiobookShelfServer.new,
|
||||||
|
name: r'audiobookShelfServerProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$audiobookShelfServerHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AudiobookShelfServer
|
||||||
|
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
22
lib/db/available_boxes.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:flutter/foundation.dart' show immutable;
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/models.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class AvailableHiveBoxes {
|
||||||
|
const AvailableHiveBoxes._();
|
||||||
|
|
||||||
|
/// Box for storing user preferences as [AppSettings]
|
||||||
|
static final userPrefsBox = Hive.box<AppSettings>(name: 'userPrefs');
|
||||||
|
|
||||||
|
/// Box for storing [ApiSettings]
|
||||||
|
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
|
||||||
|
|
||||||
|
/// stores the a list of [AudiobookShelfServer]
|
||||||
|
static final serverBox =
|
||||||
|
Hive.box<AudiobookShelfServer>(name: 'audiobookShelfServer');
|
||||||
|
|
||||||
|
/// stores the a list of [AuthenticatedUser]
|
||||||
|
static final authenticatedUserBox =
|
||||||
|
Hive.box<AuthenticatedUser>(name: 'authenticatedUser');
|
||||||
|
}
|
||||||
1009
lib/db/cache/image.g.dart
vendored
Normal file
39
lib/db/cache/schemas/image.dart
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
part '../image.g.dart';
|
||||||
|
|
||||||
|
/// Represents a cover image for a library item
|
||||||
|
///
|
||||||
|
/// stores 2 paths, one is thumbnail and the other is the full size image
|
||||||
|
/// both are optional
|
||||||
|
/// also stores last fetched date for the image
|
||||||
|
/// Id is passed as a parameter to the collection annotation (the lib_item_id)
|
||||||
|
/// also index the id
|
||||||
|
/// This is because the image is a part of the library item and the library item
|
||||||
|
/// is the parent of the image
|
||||||
|
@Collection(ignore: {'path'})
|
||||||
|
@Name('CacheImage')
|
||||||
|
class Image {
|
||||||
|
@Id()
|
||||||
|
int id;
|
||||||
|
|
||||||
|
String? thumbnailPath;
|
||||||
|
String? imagePath;
|
||||||
|
DateTime lastSaved;
|
||||||
|
|
||||||
|
Image({
|
||||||
|
required this.id,
|
||||||
|
this.thumbnailPath,
|
||||||
|
this.imagePath,
|
||||||
|
}) : lastSaved = DateTime.now();
|
||||||
|
|
||||||
|
/// returns the path to the image
|
||||||
|
String? get path => thumbnailPath ?? imagePath;
|
||||||
|
|
||||||
|
/// automatically updates the last fetched date when saving a new path
|
||||||
|
void updatePath(String? thumbnailPath, String? imagePath) async {
|
||||||
|
this.thumbnailPath = thumbnailPath;
|
||||||
|
this.imagePath = imagePath;
|
||||||
|
lastSaved = DateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/db/cache_manager.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
|
||||||
|
final imageCacheManager = CacheManager(
|
||||||
|
Config(
|
||||||
|
'image_cache_manager',
|
||||||
|
stalePeriod: const Duration(days: 365 * 10),
|
||||||
|
repo: JsonCacheInfoRepository(),
|
||||||
|
maxNrOfCacheObjects: 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final apiResponseCacheManager = CacheManager(
|
||||||
|
Config(
|
||||||
|
'api_response_cache_manager',
|
||||||
|
stalePeriod: const Duration(days: 1),
|
||||||
|
repo: JsonCacheInfoRepository(),
|
||||||
|
maxNrOfCacheObjects: 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
26
lib/db/init.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// does the initial setup of the storage
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/constants.dart';
|
||||||
|
|
||||||
|
import 'register_models.dart';
|
||||||
|
|
||||||
|
Future initStorage() async {
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
|
// use whispering_pages as the directory for hive
|
||||||
|
final storageDir = Directory(p.join(
|
||||||
|
dir.path,
|
||||||
|
AppMetadata.appName.toLowerCase().replaceAll(' ', '_'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await storageDir.create(recursive: true);
|
||||||
|
|
||||||
|
Hive.defaultDirectory = storageDir.path;
|
||||||
|
|
||||||
|
await registerModels();
|
||||||
|
}
|
||||||
22
lib/db/register_models.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/models.dart';
|
||||||
|
|
||||||
|
// register all models to Hive for serialization
|
||||||
|
Future registerModels() async {
|
||||||
|
Hive.registerAdapter<AppSettings>(
|
||||||
|
'AppSettings',
|
||||||
|
((json) => AppSettings.fromJson(json)),
|
||||||
|
);
|
||||||
|
Hive.registerAdapter<ApiSettings>(
|
||||||
|
'ApiSettings',
|
||||||
|
((json) => ApiSettings.fromJson(json)),
|
||||||
|
);
|
||||||
|
Hive.registerAdapter<AudiobookShelfServer>(
|
||||||
|
'AudiobookShelfServer',
|
||||||
|
((json) => AudiobookShelfServer.fromJson(json)),
|
||||||
|
);
|
||||||
|
Hive.registerAdapter<AuthenticatedUser>(
|
||||||
|
'AuthenticatedUser',
|
||||||
|
((json) => AuthenticatedUser.fromJson(json)),
|
||||||
|
);
|
||||||
|
}
|
||||||
3
lib/db/storage.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export 'available_boxes.dart';
|
||||||
|
export 'init.dart';
|
||||||
|
export 'register_models.dart';
|
||||||
79
lib/hacks/fix_autofill_losing_focus.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A workaround for the issue where the autofill loses focus on Android
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```dart
|
||||||
|
/// InactiveFocusScopeObserver(
|
||||||
|
/// child: FormWithTheFeildsThatMayLooseFocus(),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
// see https://github.com/flutter/flutter/issues/137760#issuecomment-1956816977
|
||||||
|
class InactiveFocusScopeObserver extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const InactiveFocusScopeObserver({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InactiveFocusScopeObserver> createState() =>
|
||||||
|
_InactiveFocusScopeObserverState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InactiveFocusScopeObserverState
|
||||||
|
extends State<InactiveFocusScopeObserver> {
|
||||||
|
final FocusScopeNode _focusScope = FocusScopeNode();
|
||||||
|
|
||||||
|
AppLifecycleListener? _listener;
|
||||||
|
FocusNode? _lastFocusedNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_registerListener();
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => FocusScope(
|
||||||
|
node: _focusScope,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_listener?.dispose();
|
||||||
|
_focusScope.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerListener() {
|
||||||
|
/// optional if you want this workaround for any platform and not just for android
|
||||||
|
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_listener = AppLifecycleListener(
|
||||||
|
onInactive: () {
|
||||||
|
_lastFocusedNode = _focusScope.focusedChild;
|
||||||
|
},
|
||||||
|
onResume: () {
|
||||||
|
_lastFocusedNode = null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_focusScope.addListener(_onFocusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChanged() {
|
||||||
|
if (_lastFocusedNode?.hasFocus == false) {
|
||||||
|
_lastFocusedNode?.requestFocus();
|
||||||
|
_lastFocusedNode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/main.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/server_provider.dart';
|
||||||
|
import 'package:whispering_pages/db/storage.dart';
|
||||||
|
import 'package:whispering_pages/pages/onboarding/onboarding.dart';
|
||||||
|
import 'package:whispering_pages/pages/pages.dart';
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/theme/theme.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// initialize the storage
|
||||||
|
await initStorage();
|
||||||
|
runApp(const ProviderScope(
|
||||||
|
child: MyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends ConsumerWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final servers = ref.watch(audiobookShelfServerProvider);
|
||||||
|
final appSettings = ref.watch(appSettingsProvider);
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
|
||||||
|
bool needOnboarding() {
|
||||||
|
return apiSettings.activeUser == null || servers.isEmpty;
|
||||||
|
}
|
||||||
|
return MaterialApp(
|
||||||
|
theme: lightTheme,
|
||||||
|
darkTheme: darkTheme,
|
||||||
|
themeMode: ref.watch(appSettingsProvider).isDarkMode
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light,
|
||||||
|
home: needOnboarding() ? const OnboardingPage() : const HomePage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/pages/app_settings.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/authenticated_user_provider.dart';
|
||||||
|
import 'package:whispering_pages/api/server_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||||
|
|
||||||
|
class AppSettingsPage extends HookConsumerWidget {
|
||||||
|
const AppSettingsPage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final appSettings = ref.watch(appSettingsProvider);
|
||||||
|
final registeredServers = ref.watch(audiobookShelfServerProvider);
|
||||||
|
final registeredServersAsList = registeredServers.toList();
|
||||||
|
final availableUsers = ref.watch(authenticatedUserProvider);
|
||||||
|
final serverURIController = useTextEditingController();
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('App Settings'),
|
||||||
|
),
|
||||||
|
body: SettingsList(
|
||||||
|
sections: [
|
||||||
|
SettingsSection(
|
||||||
|
title: const Text('Appearance'),
|
||||||
|
tiles: [
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
initialValue: appSettings.isDarkMode,
|
||||||
|
title: const Text('Dark Mode'),
|
||||||
|
leading: appSettings.isDarkMode
|
||||||
|
? const Icon(Icons.dark_mode)
|
||||||
|
: const Icon(Icons.light_mode),
|
||||||
|
onToggle: (value) {
|
||||||
|
ref.read(appSettingsProvider.notifier).toggleDarkMode();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lib/pages/home_page.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/api_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||||
|
|
||||||
|
import '../widgets/drawer.dart';
|
||||||
|
import '../widgets/shelves/home_shelf.dart';
|
||||||
|
|
||||||
|
class HomePage extends HookConsumerWidget {
|
||||||
|
const HomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// hooks for the dark mode
|
||||||
|
final settings = ref.watch(appSettingsProvider);
|
||||||
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
|
final views = ref.watch(personalizedViewProvider);
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: GestureDetector(
|
||||||
|
child: const Text('Whispering Pages'),
|
||||||
|
onTap: () {
|
||||||
|
// scroll to the top of the page
|
||||||
|
scrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
// refresh the view
|
||||||
|
ref.invalidate(personalizedViewProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
drawer: const MyDrawer(),
|
||||||
|
body: Container(
|
||||||
|
child: views.when(
|
||||||
|
data: (data) {
|
||||||
|
final shelvesToDisplay = data
|
||||||
|
.where((element) => !element.id.contains('discover'))
|
||||||
|
.map(
|
||||||
|
(shelf) => HomeShelf(
|
||||||
|
title: Text(shelf.label),
|
||||||
|
shelf: shelf,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
// await ref
|
||||||
|
// .read(personalizedViewProvider.notifier)
|
||||||
|
// .forceRefresh();
|
||||||
|
return ref.refresh(personalizedViewProvider);
|
||||||
|
},
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) => shelvesToDisplay[index],
|
||||||
|
separatorBuilder: (context, index) => Divider(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
),
|
||||||
|
itemCount: shelvesToDisplay.length,
|
||||||
|
controller: scrollController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (error, stack) {
|
||||||
|
return Text('Error: $error');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
lib/pages/onboarding/onboarding.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import 'package:coast/coast.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/pages/onboarding/server_setup.dart';
|
||||||
|
import 'package:whispering_pages/pages/onboarding/user_login.dart';
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
|
||||||
|
const _serverTag = 'server';
|
||||||
|
|
||||||
|
class OnboardingPage extends StatefulHookConsumerWidget {
|
||||||
|
const OnboardingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
OnboardingPageState createState() => OnboardingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
|
final coastController = CoastController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
coastController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
|
||||||
|
final serverUriController = useTextEditingController(
|
||||||
|
text: apiSettings.activeServer?.serverUrl.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
bool isUserLoginAvailable() {
|
||||||
|
return apiSettings.activeServer != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: invalid_use_of_protected_member
|
||||||
|
if (isUserLoginAvailable()) {
|
||||||
|
try {
|
||||||
|
coastController.animateTo(
|
||||||
|
beach: 1,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final beaches = [
|
||||||
|
Beach(
|
||||||
|
builder: (context) => FirstTimeServerSetupPage(
|
||||||
|
controller: serverUriController,
|
||||||
|
heroServerTag: _serverTag,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isUserLoginAvailable()
|
||||||
|
? Beach(
|
||||||
|
builder: (context) => FirstTimeUserLoginPage(
|
||||||
|
serverUriController: serverUriController,
|
||||||
|
heroServerTag: _serverTag,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
].nonNulls.toList();
|
||||||
|
const activeStep = 0;
|
||||||
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Coast(
|
||||||
|
beaches: beaches,
|
||||||
|
controller: coastController,
|
||||||
|
observers: [
|
||||||
|
CrabController(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
lib/pages/onboarding/server_setup.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'package:coast/coast.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/server_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/widgets/add_new_server.dart';
|
||||||
|
|
||||||
|
class FirstTimeServerSetupPage extends HookConsumerWidget {
|
||||||
|
const FirstTimeServerSetupPage({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.heroServerTag,
|
||||||
|
});
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String heroServerTag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('Welcome to Whispering Pages'),
|
||||||
|
Crab(
|
||||||
|
tag: heroServerTag,
|
||||||
|
child: AddNewServer(
|
||||||
|
controller: controller,
|
||||||
|
allowEmpty: true,
|
||||||
|
onPressed: () {
|
||||||
|
var newServer = controller.text.isEmpty
|
||||||
|
? null
|
||||||
|
: model.AudiobookShelfServer(
|
||||||
|
serverUrl: Uri.parse(controller.text),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// add the server to the list of servers
|
||||||
|
if (newServer != null) {
|
||||||
|
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
||||||
|
newServer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// else remove the server from the list of servers
|
||||||
|
else if (apiSettings.activeServer != null) {
|
||||||
|
ref
|
||||||
|
.read(audiobookShelfServerProvider.notifier)
|
||||||
|
.removeServer(apiSettings.activeServer!);
|
||||||
|
}
|
||||||
|
} on ServerAlreadyExistsException catch (e) {
|
||||||
|
newServer = e.server;
|
||||||
|
} finally {
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeServer: newServer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/pages/onboarding/user_login.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import 'package:coast/coast.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/api_provider.dart';
|
||||||
|
import 'package:whispering_pages/api/authenticated_user_provider.dart'
|
||||||
|
show authenticatedUserProvider;
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/authenticated_user.dart';
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/widgets/add_new_server.dart';
|
||||||
|
import 'package:whispering_pages/widgets/user_login.dart';
|
||||||
|
|
||||||
|
|
||||||
|
/// Once the user has selected a server, they can login to it.
|
||||||
|
class FirstTimeUserLoginPage extends HookConsumerWidget {
|
||||||
|
const FirstTimeUserLoginPage({
|
||||||
|
super.key,
|
||||||
|
required this.serverUriController,
|
||||||
|
required this.heroServerTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController serverUriController;
|
||||||
|
final String heroServerTag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final usernameController = useTextEditingController();
|
||||||
|
final passwordController = useTextEditingController();
|
||||||
|
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
final api = ref
|
||||||
|
.watch(audiobookshelfApiProvider(Uri.https(serverUriController.text)));
|
||||||
|
|
||||||
|
/// Login to the server and save the user
|
||||||
|
void loginAndSave() async {
|
||||||
|
final username = usernameController.text;
|
||||||
|
final password = passwordController.text;
|
||||||
|
final success = await api.login(username: username, password: password);
|
||||||
|
// debugPrint('Login success: $success');
|
||||||
|
if (success != null) {
|
||||||
|
var authenticatedUser = AuthenticatedUser(
|
||||||
|
server: AudiobookShelfServer(
|
||||||
|
serverUrl: Uri.parse(serverUriController.text),
|
||||||
|
),
|
||||||
|
id: success.user.id,
|
||||||
|
password: password,
|
||||||
|
username: username,
|
||||||
|
authToken: api.token!,
|
||||||
|
);
|
||||||
|
// add the user to the list of users
|
||||||
|
ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser);
|
||||||
|
|
||||||
|
// set the active user
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeUser: authenticatedUser,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Login failed'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// give focus back to the username field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Crab(
|
||||||
|
tag: heroServerTag,
|
||||||
|
child: AddNewServer(
|
||||||
|
controller: serverUriController,
|
||||||
|
onPressed: () {},
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
const Text('Login to server'),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
UserLogin(
|
||||||
|
usernameController: usernameController,
|
||||||
|
passwordController: passwordController,
|
||||||
|
onPressed: loginAndSave,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
lib/pages/pages.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export 'home_page.dart';
|
||||||
|
export 'server_manager.dart';
|
||||||
131
lib/pages/server_manager.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/authenticated_user_provider.dart';
|
||||||
|
import 'package:whispering_pages/api/server_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/widgets/add_new_server.dart';
|
||||||
|
|
||||||
|
class ServerManagerPage extends HookConsumerWidget {
|
||||||
|
const ServerManagerPage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
final registeredServers = ref.watch(audiobookShelfServerProvider);
|
||||||
|
final registeredServersAsList = registeredServers.toList();
|
||||||
|
final availableUsers = ref.watch(authenticatedUserProvider);
|
||||||
|
final serverURIController = useTextEditingController();
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
debugPrint('registered servers: $registeredServers');
|
||||||
|
debugPrint('available users: $availableUsers');
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Setup Servers'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Registered Servers',
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: registeredServers.length,
|
||||||
|
reverse: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
var registeredServer = registeredServersAsList[index];
|
||||||
|
return ExpansionTile(
|
||||||
|
title: Text(registeredServer.serverUrl.toString()),
|
||||||
|
subtitle: Text(
|
||||||
|
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
// delete the server from the list of servers
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(audiobookShelfServerProvider.notifier)
|
||||||
|
.removeServer(registeredServer);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// children are list of users of this server
|
||||||
|
children: availableUsers
|
||||||
|
.where(
|
||||||
|
(element) => element.server == registeredServer,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(e) => ListTile(
|
||||||
|
title: Text(e.username ?? 'Anonymous'),
|
||||||
|
subtitle: Text(e.authToken),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(authenticatedUserProvider.notifier)
|
||||||
|
.removeUser(e);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.nonNulls
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: AddNewServer(
|
||||||
|
controller: serverURIController,
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
try {
|
||||||
|
final newServer = model.AudiobookShelfServer(
|
||||||
|
serverUrl: Uri.parse(serverURIController.text),
|
||||||
|
);
|
||||||
|
ref
|
||||||
|
.read(audiobookShelfServerProvider.notifier)
|
||||||
|
.addServer(
|
||||||
|
newServer,
|
||||||
|
);
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeServer: newServer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
serverURIController.clear();
|
||||||
|
} on ServerAlreadyExistsException catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Invalid URL'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lib/settings/api_settings_provider.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// this provider is used to provide the Api settings to the app
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/api_settings.dart' as model;
|
||||||
|
import 'package:whispering_pages/db/available_boxes.dart';
|
||||||
|
|
||||||
|
part 'api_settings_provider.g.dart';
|
||||||
|
|
||||||
|
final _box = AvailableHiveBoxes.apiSettingsBox;
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class ApiSettings extends _$ApiSettings {
|
||||||
|
@override
|
||||||
|
model.ApiSettings build() {
|
||||||
|
state = readFromBoxOrCreate();
|
||||||
|
ref.listenSelf((_, __) {
|
||||||
|
writeToBox();
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.ApiSettings readFromBoxOrCreate() {
|
||||||
|
// see if the settings are already in the box
|
||||||
|
if (_box.isNotEmpty) {
|
||||||
|
final foundSettings = _box.getAt(0);
|
||||||
|
debugPrint('found api settings in box: $foundSettings');
|
||||||
|
return foundSettings;
|
||||||
|
} else {
|
||||||
|
// create a new settings object
|
||||||
|
const settings = model.ApiSettings();
|
||||||
|
debugPrint('created new api settings: $settings');
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the settings to the box
|
||||||
|
void writeToBox() {
|
||||||
|
_box.clear();
|
||||||
|
_box.add(state);
|
||||||
|
debugPrint('wrote api settings to box: $state');
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateState(model.ApiSettings newSettings) {
|
||||||
|
state = newSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/settings/api_settings_provider.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'api_settings_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$apiSettingsHash() => r'a6927751bd91ec7c9e1a2810dc939407d9112210';
|
||||||
|
|
||||||
|
/// See also [ApiSettings].
|
||||||
|
@ProviderFor(ApiSettings)
|
||||||
|
final apiSettingsProvider =
|
||||||
|
AutoDisposeNotifierProvider<ApiSettings, model.ApiSettings>.internal(
|
||||||
|
ApiSettings.new,
|
||||||
|
name: r'apiSettingsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$ApiSettings = AutoDisposeNotifier<model.ApiSettings>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
51
lib/settings/app_settings_provider.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
// this provider is used to provide the app settings to the app
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
|
||||||
|
import 'package:whispering_pages/db/available_boxes.dart';
|
||||||
|
|
||||||
|
part 'app_settings_provider.g.dart';
|
||||||
|
|
||||||
|
final _box = AvailableHiveBoxes.userPrefsBox;
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class AppSettings extends _$AppSettings {
|
||||||
|
@override
|
||||||
|
model.AppSettings build() {
|
||||||
|
state = readFromBoxOrCreate();
|
||||||
|
ref.listenSelf((_, __) {
|
||||||
|
writeToBox();
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.AppSettings readFromBoxOrCreate() {
|
||||||
|
// see if the settings are already in the box
|
||||||
|
if (_box.isNotEmpty) {
|
||||||
|
final foundSettings = _box.getAt(0);
|
||||||
|
debugPrint('found settings in box: $foundSettings');
|
||||||
|
return foundSettings;
|
||||||
|
} else {
|
||||||
|
// create a new settings object
|
||||||
|
const settings = model.AppSettings();
|
||||||
|
debugPrint('created new settings: $settings');
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the settings to the box
|
||||||
|
void writeToBox() {
|
||||||
|
_box.clear();
|
||||||
|
_box.add(state);
|
||||||
|
debugPrint('wrote settings to box: $state');
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleDarkMode() {
|
||||||
|
state = state.copyWith(isDarkMode: !state.isDarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateState(model.AppSettings newSettings) {
|
||||||
|
state = newSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/settings/app_settings_provider.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'app_settings_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$appSettingsHash() => r'da2cd1bb0da6e136e906bc61f29da89d0c5f53fb';
|
||||||
|
|
||||||
|
/// See also [AppSettings].
|
||||||
|
@ProviderFor(AppSettings)
|
||||||
|
final appSettingsProvider =
|
||||||
|
AutoDisposeNotifierProvider<AppSettings, model.AppSettings>.internal(
|
||||||
|
AppSettings.new,
|
||||||
|
name: r'appSettingsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AppSettings = AutoDisposeNotifier<model.AppSettings>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
8
lib/settings/constants.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'package:flutter/foundation.dart' show immutable;
|
||||||
|
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class AppMetadata {
|
||||||
|
const AppMetadata._();
|
||||||
|
static const String appName = 'Whispering Pages';
|
||||||
|
}
|
||||||
23
lib/settings/models/api_settings.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// a freezed class to store the settings of the app
|
||||||
|
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/authenticated_user.dart';
|
||||||
|
|
||||||
|
part 'api_settings.freezed.dart';
|
||||||
|
part 'api_settings.g.dart';
|
||||||
|
|
||||||
|
/// stores the settings for the active server and user
|
||||||
|
///
|
||||||
|
/// all settings that are needed to interact with the server are stored here
|
||||||
|
@freezed
|
||||||
|
class ApiSettings with _$ApiSettings {
|
||||||
|
const factory ApiSettings({
|
||||||
|
AudiobookShelfServer? activeServer,
|
||||||
|
AuthenticatedUser? activeUser,
|
||||||
|
String? activeLibraryId,
|
||||||
|
}) = _ApiSettings;
|
||||||
|
|
||||||
|
factory ApiSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ApiSettingsFromJson(json);
|
||||||
|
}
|
||||||
229
lib/settings/models/api_settings.freezed.dart
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'api_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
ApiSettings _$ApiSettingsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _ApiSettings.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$ApiSettings {
|
||||||
|
AudiobookShelfServer? get activeServer => throw _privateConstructorUsedError;
|
||||||
|
AuthenticatedUser? get activeUser => throw _privateConstructorUsedError;
|
||||||
|
String? get activeLibraryId => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$ApiSettingsCopyWith<ApiSettings> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $ApiSettingsCopyWith<$Res> {
|
||||||
|
factory $ApiSettingsCopyWith(
|
||||||
|
ApiSettings value, $Res Function(ApiSettings) then) =
|
||||||
|
_$ApiSettingsCopyWithImpl<$Res, ApiSettings>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{AudiobookShelfServer? activeServer,
|
||||||
|
AuthenticatedUser? activeUser,
|
||||||
|
String? activeLibraryId});
|
||||||
|
|
||||||
|
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
|
||||||
|
$AuthenticatedUserCopyWith<$Res>? get activeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
|
||||||
|
implements $ApiSettingsCopyWith<$Res> {
|
||||||
|
_$ApiSettingsCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? activeServer = freezed,
|
||||||
|
Object? activeUser = freezed,
|
||||||
|
Object? activeLibraryId = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
activeServer: freezed == activeServer
|
||||||
|
? _value.activeServer
|
||||||
|
: activeServer // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AudiobookShelfServer?,
|
||||||
|
activeUser: freezed == activeUser
|
||||||
|
? _value.activeUser
|
||||||
|
: activeUser // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AuthenticatedUser?,
|
||||||
|
activeLibraryId: freezed == activeLibraryId
|
||||||
|
? _value.activeLibraryId
|
||||||
|
: activeLibraryId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$AudiobookShelfServerCopyWith<$Res>? get activeServer {
|
||||||
|
if (_value.activeServer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $AudiobookShelfServerCopyWith<$Res>(_value.activeServer!, (value) {
|
||||||
|
return _then(_value.copyWith(activeServer: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$AuthenticatedUserCopyWith<$Res>? get activeUser {
|
||||||
|
if (_value.activeUser == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $AuthenticatedUserCopyWith<$Res>(_value.activeUser!, (value) {
|
||||||
|
return _then(_value.copyWith(activeUser: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$ApiSettingsImplCopyWith<$Res>
|
||||||
|
implements $ApiSettingsCopyWith<$Res> {
|
||||||
|
factory _$$ApiSettingsImplCopyWith(
|
||||||
|
_$ApiSettingsImpl value, $Res Function(_$ApiSettingsImpl) then) =
|
||||||
|
__$$ApiSettingsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{AudiobookShelfServer? activeServer,
|
||||||
|
AuthenticatedUser? activeUser,
|
||||||
|
String? activeLibraryId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
|
||||||
|
@override
|
||||||
|
$AuthenticatedUserCopyWith<$Res>? get activeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$ApiSettingsImplCopyWithImpl<$Res>
|
||||||
|
extends _$ApiSettingsCopyWithImpl<$Res, _$ApiSettingsImpl>
|
||||||
|
implements _$$ApiSettingsImplCopyWith<$Res> {
|
||||||
|
__$$ApiSettingsImplCopyWithImpl(
|
||||||
|
_$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? activeServer = freezed,
|
||||||
|
Object? activeUser = freezed,
|
||||||
|
Object? activeLibraryId = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$ApiSettingsImpl(
|
||||||
|
activeServer: freezed == activeServer
|
||||||
|
? _value.activeServer
|
||||||
|
: activeServer // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AudiobookShelfServer?,
|
||||||
|
activeUser: freezed == activeUser
|
||||||
|
? _value.activeUser
|
||||||
|
: activeUser // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AuthenticatedUser?,
|
||||||
|
activeLibraryId: freezed == activeLibraryId
|
||||||
|
? _value.activeLibraryId
|
||||||
|
: activeLibraryId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$ApiSettingsImpl implements _ApiSettings {
|
||||||
|
const _$ApiSettingsImpl(
|
||||||
|
{this.activeServer, this.activeUser, this.activeLibraryId});
|
||||||
|
|
||||||
|
factory _$ApiSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$ApiSettingsImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final AudiobookShelfServer? activeServer;
|
||||||
|
@override
|
||||||
|
final AuthenticatedUser? activeUser;
|
||||||
|
@override
|
||||||
|
final String? activeLibraryId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$ApiSettingsImpl &&
|
||||||
|
(identical(other.activeServer, activeServer) ||
|
||||||
|
other.activeServer == activeServer) &&
|
||||||
|
(identical(other.activeUser, activeUser) ||
|
||||||
|
other.activeUser == activeUser) &&
|
||||||
|
(identical(other.activeLibraryId, activeLibraryId) ||
|
||||||
|
other.activeLibraryId == activeLibraryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, activeServer, activeUser, activeLibraryId);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
|
||||||
|
__$$ApiSettingsImplCopyWithImpl<_$ApiSettingsImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$ApiSettingsImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _ApiSettings implements ApiSettings {
|
||||||
|
const factory _ApiSettings(
|
||||||
|
{final AudiobookShelfServer? activeServer,
|
||||||
|
final AuthenticatedUser? activeUser,
|
||||||
|
final String? activeLibraryId}) = _$ApiSettingsImpl;
|
||||||
|
|
||||||
|
factory _ApiSettings.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$ApiSettingsImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AudiobookShelfServer? get activeServer;
|
||||||
|
@override
|
||||||
|
AuthenticatedUser? get activeUser;
|
||||||
|
@override
|
||||||
|
String? get activeLibraryId;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
27
lib/settings/models/api_settings.g.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'api_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ApiSettingsImpl(
|
||||||
|
activeServer: json['activeServer'] == null
|
||||||
|
? null
|
||||||
|
: AudiobookShelfServer.fromJson(
|
||||||
|
json['activeServer'] as Map<String, dynamic>),
|
||||||
|
activeUser: json['activeUser'] == null
|
||||||
|
? null
|
||||||
|
: AuthenticatedUser.fromJson(
|
||||||
|
json['activeUser'] as Map<String, dynamic>),
|
||||||
|
activeLibraryId: json['activeLibraryId'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'activeServer': instance.activeServer,
|
||||||
|
'activeUser': instance.activeUser,
|
||||||
|
'activeLibraryId': instance.activeLibraryId,
|
||||||
|
};
|
||||||
19
lib/settings/models/app_settings.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// a freezed class to store the settings of the app
|
||||||
|
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'app_settings.freezed.dart';
|
||||||
|
part 'app_settings.g.dart';
|
||||||
|
|
||||||
|
/// stores the settings of the app
|
||||||
|
///
|
||||||
|
/// only the visual settings are stored here
|
||||||
|
@freezed
|
||||||
|
class AppSettings with _$AppSettings {
|
||||||
|
const factory AppSettings({
|
||||||
|
@Default(true) bool isDarkMode,
|
||||||
|
}) = _AppSettings;
|
||||||
|
|
||||||
|
factory AppSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AppSettingsFromJson(json);
|
||||||
|
}
|
||||||
153
lib/settings/models/app_settings.freezed.dart
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'app_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _AppSettings.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$AppSettings {
|
||||||
|
bool get isDarkMode => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$AppSettingsCopyWith<AppSettings> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $AppSettingsCopyWith<$Res> {
|
||||||
|
factory $AppSettingsCopyWith(
|
||||||
|
AppSettings value, $Res Function(AppSettings) then) =
|
||||||
|
_$AppSettingsCopyWithImpl<$Res, AppSettings>;
|
||||||
|
@useResult
|
||||||
|
$Res call({bool isDarkMode});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
|
||||||
|
implements $AppSettingsCopyWith<$Res> {
|
||||||
|
_$AppSettingsCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? isDarkMode = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
isDarkMode: null == isDarkMode
|
||||||
|
? _value.isDarkMode
|
||||||
|
: isDarkMode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AppSettingsImplCopyWith<$Res>
|
||||||
|
implements $AppSettingsCopyWith<$Res> {
|
||||||
|
factory _$$AppSettingsImplCopyWith(
|
||||||
|
_$AppSettingsImpl value, $Res Function(_$AppSettingsImpl) then) =
|
||||||
|
__$$AppSettingsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({bool isDarkMode});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AppSettingsImplCopyWithImpl<$Res>
|
||||||
|
extends _$AppSettingsCopyWithImpl<$Res, _$AppSettingsImpl>
|
||||||
|
implements _$$AppSettingsImplCopyWith<$Res> {
|
||||||
|
__$$AppSettingsImplCopyWithImpl(
|
||||||
|
_$AppSettingsImpl _value, $Res Function(_$AppSettingsImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? isDarkMode = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$AppSettingsImpl(
|
||||||
|
isDarkMode: null == isDarkMode
|
||||||
|
? _value.isDarkMode
|
||||||
|
: isDarkMode // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$AppSettingsImpl implements _AppSettings {
|
||||||
|
const _$AppSettingsImpl({this.isDarkMode = true});
|
||||||
|
|
||||||
|
factory _$AppSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$AppSettingsImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool isDarkMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AppSettings(isDarkMode: $isDarkMode)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AppSettingsImpl &&
|
||||||
|
(identical(other.isDarkMode, isDarkMode) ||
|
||||||
|
other.isDarkMode == isDarkMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, isDarkMode);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
|
||||||
|
__$$AppSettingsImplCopyWithImpl<_$AppSettingsImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$AppSettingsImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _AppSettings implements AppSettings {
|
||||||
|
const factory _AppSettings({final bool isDarkMode}) = _$AppSettingsImpl;
|
||||||
|
|
||||||
|
factory _AppSettings.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$AppSettingsImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isDarkMode;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
17
lib/settings/models/app_settings.g.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'app_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$AppSettingsImpl _$$AppSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AppSettingsImpl(
|
||||||
|
isDarkMode: json['isDarkMode'] as bool? ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'isDarkMode': instance.isDarkMode,
|
||||||
|
};
|
||||||
18
lib/settings/models/audiobookshelf_server.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'audiobookshelf_server.freezed.dart';
|
||||||
|
part 'audiobookshelf_server.g.dart';
|
||||||
|
|
||||||
|
typedef AudiobookShelfUri = Uri;
|
||||||
|
|
||||||
|
/// Represents a audiobookshelf server
|
||||||
|
@freezed
|
||||||
|
class AudiobookShelfServer with _$AudiobookShelfServer {
|
||||||
|
const factory AudiobookShelfServer({
|
||||||
|
required AudiobookShelfUri serverUrl,
|
||||||
|
// String? serverName,
|
||||||
|
}) = _AudiobookShelfServer;
|
||||||
|
|
||||||
|
factory AudiobookShelfServer.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AudiobookShelfServerFromJson(json);
|
||||||
|
}
|
||||||
156
lib/settings/models/audiobookshelf_server.freezed.dart
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'audiobookshelf_server.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
AudiobookShelfServer _$AudiobookShelfServerFromJson(Map<String, dynamic> json) {
|
||||||
|
return _AudiobookShelfServer.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$AudiobookShelfServer {
|
||||||
|
Uri get serverUrl => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$AudiobookShelfServerCopyWith<AudiobookShelfServer> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $AudiobookShelfServerCopyWith<$Res> {
|
||||||
|
factory $AudiobookShelfServerCopyWith(AudiobookShelfServer value,
|
||||||
|
$Res Function(AudiobookShelfServer) then) =
|
||||||
|
_$AudiobookShelfServerCopyWithImpl<$Res, AudiobookShelfServer>;
|
||||||
|
@useResult
|
||||||
|
$Res call({Uri serverUrl});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$AudiobookShelfServerCopyWithImpl<$Res,
|
||||||
|
$Val extends AudiobookShelfServer>
|
||||||
|
implements $AudiobookShelfServerCopyWith<$Res> {
|
||||||
|
_$AudiobookShelfServerCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? serverUrl = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
serverUrl: null == serverUrl
|
||||||
|
? _value.serverUrl
|
||||||
|
: serverUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Uri,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AudiobookShelfServerImplCopyWith<$Res>
|
||||||
|
implements $AudiobookShelfServerCopyWith<$Res> {
|
||||||
|
factory _$$AudiobookShelfServerImplCopyWith(_$AudiobookShelfServerImpl value,
|
||||||
|
$Res Function(_$AudiobookShelfServerImpl) then) =
|
||||||
|
__$$AudiobookShelfServerImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({Uri serverUrl});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AudiobookShelfServerImplCopyWithImpl<$Res>
|
||||||
|
extends _$AudiobookShelfServerCopyWithImpl<$Res, _$AudiobookShelfServerImpl>
|
||||||
|
implements _$$AudiobookShelfServerImplCopyWith<$Res> {
|
||||||
|
__$$AudiobookShelfServerImplCopyWithImpl(_$AudiobookShelfServerImpl _value,
|
||||||
|
$Res Function(_$AudiobookShelfServerImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? serverUrl = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$AudiobookShelfServerImpl(
|
||||||
|
serverUrl: null == serverUrl
|
||||||
|
? _value.serverUrl
|
||||||
|
: serverUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Uri,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$AudiobookShelfServerImpl implements _AudiobookShelfServer {
|
||||||
|
const _$AudiobookShelfServerImpl({required this.serverUrl});
|
||||||
|
|
||||||
|
factory _$AudiobookShelfServerImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$AudiobookShelfServerImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Uri serverUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AudiobookShelfServer(serverUrl: $serverUrl)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AudiobookShelfServerImpl &&
|
||||||
|
(identical(other.serverUrl, serverUrl) ||
|
||||||
|
other.serverUrl == serverUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, serverUrl);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
|
||||||
|
get copyWith =>
|
||||||
|
__$$AudiobookShelfServerImplCopyWithImpl<_$AudiobookShelfServerImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$AudiobookShelfServerImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _AudiobookShelfServer implements AudiobookShelfServer {
|
||||||
|
const factory _AudiobookShelfServer({required final Uri serverUrl}) =
|
||||||
|
_$AudiobookShelfServerImpl;
|
||||||
|
|
||||||
|
factory _AudiobookShelfServer.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$AudiobookShelfServerImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Uri get serverUrl;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
19
lib/settings/models/audiobookshelf_server.g.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'audiobookshelf_server.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$AudiobookShelfServerImpl(
|
||||||
|
serverUrl: Uri.parse(json['serverUrl'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$AudiobookShelfServerImplToJson(
|
||||||
|
_$AudiobookShelfServerImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'serverUrl': instance.serverUrl.toString(),
|
||||||
|
};
|
||||||
20
lib/settings/models/authenticated_user.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||||
|
|
||||||
|
part 'authenticated_user.freezed.dart';
|
||||||
|
part 'authenticated_user.g.dart';
|
||||||
|
|
||||||
|
/// authenticated user with server and credentials
|
||||||
|
@freezed
|
||||||
|
class AuthenticatedUser with _$AuthenticatedUser {
|
||||||
|
const factory AuthenticatedUser({
|
||||||
|
required AudiobookShelfServer server,
|
||||||
|
required String authToken,
|
||||||
|
String? id,
|
||||||
|
String? username,
|
||||||
|
String? password,
|
||||||
|
}) = _AuthenticatedUser;
|
||||||
|
|
||||||
|
factory AuthenticatedUser.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AuthenticatedUserFromJson(json);
|
||||||
|
}
|
||||||
253
lib/settings/models/authenticated_user.freezed.dart
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'authenticated_user.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
AuthenticatedUser _$AuthenticatedUserFromJson(Map<String, dynamic> json) {
|
||||||
|
return _AuthenticatedUser.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$AuthenticatedUser {
|
||||||
|
AudiobookShelfServer get server => throw _privateConstructorUsedError;
|
||||||
|
String get authToken => throw _privateConstructorUsedError;
|
||||||
|
String? get id => throw _privateConstructorUsedError;
|
||||||
|
String? get username => throw _privateConstructorUsedError;
|
||||||
|
String? get password => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$AuthenticatedUserCopyWith<AuthenticatedUser> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $AuthenticatedUserCopyWith<$Res> {
|
||||||
|
factory $AuthenticatedUserCopyWith(
|
||||||
|
AuthenticatedUser value, $Res Function(AuthenticatedUser) then) =
|
||||||
|
_$AuthenticatedUserCopyWithImpl<$Res, AuthenticatedUser>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{AudiobookShelfServer server,
|
||||||
|
String authToken,
|
||||||
|
String? id,
|
||||||
|
String? username,
|
||||||
|
String? password});
|
||||||
|
|
||||||
|
$AudiobookShelfServerCopyWith<$Res> get server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
|
||||||
|
implements $AuthenticatedUserCopyWith<$Res> {
|
||||||
|
_$AuthenticatedUserCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? server = null,
|
||||||
|
Object? authToken = null,
|
||||||
|
Object? id = freezed,
|
||||||
|
Object? username = freezed,
|
||||||
|
Object? password = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
server: null == server
|
||||||
|
? _value.server
|
||||||
|
: server // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AudiobookShelfServer,
|
||||||
|
authToken: null == authToken
|
||||||
|
? _value.authToken
|
||||||
|
: authToken // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
id: freezed == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
username: freezed == username
|
||||||
|
? _value.username
|
||||||
|
: username // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
password: freezed == password
|
||||||
|
? _value.password
|
||||||
|
: password // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$AudiobookShelfServerCopyWith<$Res> get server {
|
||||||
|
return $AudiobookShelfServerCopyWith<$Res>(_value.server, (value) {
|
||||||
|
return _then(_value.copyWith(server: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AuthenticatedUserImplCopyWith<$Res>
|
||||||
|
implements $AuthenticatedUserCopyWith<$Res> {
|
||||||
|
factory _$$AuthenticatedUserImplCopyWith(_$AuthenticatedUserImpl value,
|
||||||
|
$Res Function(_$AuthenticatedUserImpl) then) =
|
||||||
|
__$$AuthenticatedUserImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{AudiobookShelfServer server,
|
||||||
|
String authToken,
|
||||||
|
String? id,
|
||||||
|
String? username,
|
||||||
|
String? password});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$AudiobookShelfServerCopyWith<$Res> get server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AuthenticatedUserImplCopyWithImpl<$Res>
|
||||||
|
extends _$AuthenticatedUserCopyWithImpl<$Res, _$AuthenticatedUserImpl>
|
||||||
|
implements _$$AuthenticatedUserImplCopyWith<$Res> {
|
||||||
|
__$$AuthenticatedUserImplCopyWithImpl(_$AuthenticatedUserImpl _value,
|
||||||
|
$Res Function(_$AuthenticatedUserImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? server = null,
|
||||||
|
Object? authToken = null,
|
||||||
|
Object? id = freezed,
|
||||||
|
Object? username = freezed,
|
||||||
|
Object? password = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$AuthenticatedUserImpl(
|
||||||
|
server: null == server
|
||||||
|
? _value.server
|
||||||
|
: server // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AudiobookShelfServer,
|
||||||
|
authToken: null == authToken
|
||||||
|
? _value.authToken
|
||||||
|
: authToken // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
id: freezed == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
username: freezed == username
|
||||||
|
? _value.username
|
||||||
|
: username // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
password: freezed == password
|
||||||
|
? _value.password
|
||||||
|
: password // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$AuthenticatedUserImpl implements _AuthenticatedUser {
|
||||||
|
const _$AuthenticatedUserImpl(
|
||||||
|
{required this.server,
|
||||||
|
required this.authToken,
|
||||||
|
this.id,
|
||||||
|
this.username,
|
||||||
|
this.password});
|
||||||
|
|
||||||
|
factory _$AuthenticatedUserImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$AuthenticatedUserImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final AudiobookShelfServer server;
|
||||||
|
@override
|
||||||
|
final String authToken;
|
||||||
|
@override
|
||||||
|
final String? id;
|
||||||
|
@override
|
||||||
|
final String? username;
|
||||||
|
@override
|
||||||
|
final String? password;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username, password: $password)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AuthenticatedUserImpl &&
|
||||||
|
(identical(other.server, server) || other.server == server) &&
|
||||||
|
(identical(other.authToken, authToken) ||
|
||||||
|
other.authToken == authToken) &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.username, username) ||
|
||||||
|
other.username == username) &&
|
||||||
|
(identical(other.password, password) ||
|
||||||
|
other.password == password));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, server, authToken, id, username, password);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
|
||||||
|
__$$AuthenticatedUserImplCopyWithImpl<_$AuthenticatedUserImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$AuthenticatedUserImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _AuthenticatedUser implements AuthenticatedUser {
|
||||||
|
const factory _AuthenticatedUser(
|
||||||
|
{required final AudiobookShelfServer server,
|
||||||
|
required final String authToken,
|
||||||
|
final String? id,
|
||||||
|
final String? username,
|
||||||
|
final String? password}) = _$AuthenticatedUserImpl;
|
||||||
|
|
||||||
|
factory _AuthenticatedUser.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$AuthenticatedUserImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AudiobookShelfServer get server;
|
||||||
|
@override
|
||||||
|
String get authToken;
|
||||||
|
@override
|
||||||
|
String? get id;
|
||||||
|
@override
|
||||||
|
String? get username;
|
||||||
|
@override
|
||||||
|
String? get password;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
28
lib/settings/models/authenticated_user.g.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'authenticated_user.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$AuthenticatedUserImpl(
|
||||||
|
server:
|
||||||
|
AudiobookShelfServer.fromJson(json['server'] as Map<String, dynamic>),
|
||||||
|
authToken: json['authToken'] as String,
|
||||||
|
id: json['id'] as String?,
|
||||||
|
username: json['username'] as String?,
|
||||||
|
password: json['password'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$AuthenticatedUserImplToJson(
|
||||||
|
_$AuthenticatedUserImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'server': instance.server,
|
||||||
|
'authToken': instance.authToken,
|
||||||
|
'id': instance.id,
|
||||||
|
'username': instance.username,
|
||||||
|
'password': instance.password,
|
||||||
|
};
|
||||||
4
lib/settings/models/models.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export 'api_settings.dart';
|
||||||
|
export 'app_settings.dart';
|
||||||
|
export 'audiobookshelf_server.dart';
|
||||||
|
export 'authenticated_user.dart';
|
||||||
2
lib/settings/settings.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export 'app_settings_provider.dart';
|
||||||
|
export 'constants.dart';
|
||||||
8
lib/theme/dark.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final ThemeData darkTheme = ThemeData(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: ColorScheme.dark(
|
||||||
|
background: Colors.grey[900]!,
|
||||||
|
),
|
||||||
|
);
|
||||||
8
lib/theme/light.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final ThemeData lightTheme = ThemeData(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
colorScheme: ColorScheme.light(
|
||||||
|
background: Colors.grey[200]!,
|
||||||
|
),
|
||||||
|
);
|
||||||
2
lib/theme/theme.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export 'dark.dart';
|
||||||
|
export 'light.dart';
|
||||||
107
lib/widgets/add_new_server.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:whispering_pages/api/api_provider.dart';
|
||||||
|
|
||||||
|
class AddNewServer extends HookConsumerWidget {
|
||||||
|
const AddNewServer({
|
||||||
|
super.key,
|
||||||
|
this.controller,
|
||||||
|
this.onPressed,
|
||||||
|
this.readOnly = false,
|
||||||
|
this.allowEmpty = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
/// the function to call when the button is pressed
|
||||||
|
final void Function()? onPressed;
|
||||||
|
|
||||||
|
/// if this field is read only
|
||||||
|
final bool readOnly;
|
||||||
|
|
||||||
|
/// the server URI can be empty
|
||||||
|
final bool allowEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final myController = controller ?? useTextEditingController();
|
||||||
|
var newServerURI = useValueListenable(myController);
|
||||||
|
final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text));
|
||||||
|
bool isServerAliveValue = isServerAlive.when(
|
||||||
|
data: (value) => value,
|
||||||
|
loading: () => false,
|
||||||
|
error: (error, _) => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: readOnly,
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
autofillHints: const [AutofillHints.url],
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Server URI',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixText: 'https://',
|
||||||
|
prefixIcon: Tooltip(
|
||||||
|
message: newServerURI.text.isEmpty
|
||||||
|
? 'Server Status'
|
||||||
|
: isServerAliveValue
|
||||||
|
? 'Server connected'
|
||||||
|
: 'Cannot connect to server',
|
||||||
|
child: newServerURI.text.isEmpty
|
||||||
|
? Icon(
|
||||||
|
Icons.cloud_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
: isServerAlive.when(
|
||||||
|
data: (value) {
|
||||||
|
return value
|
||||||
|
? Icon(
|
||||||
|
Icons.cloud_done_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.cloud_off_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => Transform.scale(
|
||||||
|
scale: 0.5,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, _) => Icon(
|
||||||
|
Icons.cloud_off_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// add server button
|
||||||
|
suffixIcon: onPressed == null
|
||||||
|
? null
|
||||||
|
: Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||||
|
child: IconButton.filled(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Add new server',
|
||||||
|
color: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
focusColor: Theme.of(context).colorScheme.onBackground,
|
||||||
|
|
||||||
|
// should be enabled when
|
||||||
|
onPressed: !readOnly &&
|
||||||
|
(isServerAliveValue ||
|
||||||
|
(allowEmpty && newServerURI.text.isEmpty))
|
||||||
|
? onPressed
|
||||||
|
: null, // disable button if server is not alive
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// add to add to existing servers
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/widgets/drawer.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:whispering_pages/pages/app_settings.dart';
|
||||||
|
import 'package:whispering_pages/pages/server_manager.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class MyDrawer extends StatelessWidget {
|
||||||
|
const MyDrawer({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
const DrawerHeader(
|
||||||
|
child: Text(
|
||||||
|
'Whispering Pages',
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('server Settings'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const ServerManagerPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('App Settings'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const AppSettingsPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
lib/widgets/shelves/author_shelf.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
import 'package:whispering_pages/api/image_provider.dart';
|
||||||
|
import 'package:whispering_pages/widgets/shelves/home_shelf.dart';
|
||||||
|
|
||||||
|
/// A shelf that displays Authors on the home page
|
||||||
|
class AuthorHomeShelf extends HookConsumerWidget {
|
||||||
|
const AuthorHomeShelf({
|
||||||
|
super.key,
|
||||||
|
required this.shelf,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final AuthorShelf shelf;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return SimpleHomeShelf(
|
||||||
|
title: title,
|
||||||
|
children: shelf.entities
|
||||||
|
.map(
|
||||||
|
(item) => AuthorOnShelf(item: item),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a widget to display a item on the shelf
|
||||||
|
class AuthorOnShelf extends HookConsumerWidget {
|
||||||
|
const AuthorOnShelf({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Author item;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final author = AuthorMinified.fromJson(item.toJson());
|
||||||
|
// final coverImage = ref.watch(coverImageProvider(item));
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 100),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 50),
|
||||||
|
// child: coverImage.when(
|
||||||
|
// data: (image) {
|
||||||
|
// return Image.memory(image, fit: BoxFit.cover);
|
||||||
|
// },
|
||||||
|
// loading: () {
|
||||||
|
// return const Center(child: CircularProgressIndicator());
|
||||||
|
// },
|
||||||
|
// error: (error, stack) {
|
||||||
|
// return const Icon(Icons.error);
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(5),
|
||||||
|
child: Text(
|
||||||
|
author.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
lib/widgets/shelves/book_shelf.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import 'package:auto_scroll_text/auto_scroll_text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
import 'package:whispering_pages/api/image_provider.dart';
|
||||||
|
import 'package:whispering_pages/widgets/shelves/home_shelf.dart';
|
||||||
|
|
||||||
|
/// A shelf that displays books on the home page
|
||||||
|
class BookHomeShelf extends HookConsumerWidget {
|
||||||
|
const BookHomeShelf({
|
||||||
|
super.key,
|
||||||
|
required this.shelf,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final LibraryItemShelf shelf;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return SimpleHomeShelf(
|
||||||
|
title: title,
|
||||||
|
children: shelf.entities
|
||||||
|
.map(
|
||||||
|
(item) => switch (item.mediaType) {
|
||||||
|
MediaType.book => BookOnShelf(
|
||||||
|
item: item,
|
||||||
|
key: ValueKey(shelf.id + item.id),
|
||||||
|
),
|
||||||
|
_ => Container(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a widget to display a item on the shelf
|
||||||
|
class BookOnShelf extends HookConsumerWidget {
|
||||||
|
const BookOnShelf({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
final LibraryItem item;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final book = BookMinified.fromJson(item.media.toJson());
|
||||||
|
final metadata = BookMetadataMinified.fromJson(book.metadata.toJson());
|
||||||
|
final coverImage = ref.watch(coverImageProvider(item));
|
||||||
|
const coverSize = 150.0;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
||||||
|
constraints: const BoxConstraints(maxWidth: coverSize),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: coverSize),
|
||||||
|
color: Colors.grey[800],
|
||||||
|
child: coverImage.when(
|
||||||
|
data: (image) {
|
||||||
|
if (image.isEmpty) {
|
||||||
|
return const Icon(Icons.error);
|
||||||
|
}
|
||||||
|
return Image.memory(
|
||||||
|
image,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
cacheWidth:
|
||||||
|
(coverSize * MediaQuery.of(context).devicePixelRatio)
|
||||||
|
.round(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
},
|
||||||
|
error: (error, stack) {
|
||||||
|
return const Icon(Icons.error);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AutoScrollText(
|
||||||
|
metadata.title ?? '',
|
||||||
|
mode: AutoScrollTextMode.bouncing,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
velocity: const Velocity(pixelsPerSecond: Offset(15, 0)),
|
||||||
|
delayBefore: const Duration(seconds: 2),
|
||||||
|
pauseBetween: const Duration(seconds: 2),
|
||||||
|
numberOfReps: 15,
|
||||||
|
// maxLines: 1,
|
||||||
|
// overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
metadata.authorName ?? '',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
lib/widgets/shelves/home_shelf.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
import 'package:whispering_pages/widgets/shelves/author_shelf.dart';
|
||||||
|
import 'package:whispering_pages/widgets/shelves/book_shelf.dart';
|
||||||
|
|
||||||
|
/// A shelf that displays books/authors/series on the home page
|
||||||
|
///
|
||||||
|
/// this will build the appropriate shelf based on the type of the shelf
|
||||||
|
class HomeShelf extends HookConsumerWidget {
|
||||||
|
const HomeShelf({
|
||||||
|
super.key,
|
||||||
|
required this.shelf,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final Shelf shelf;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return switch (shelf.type) {
|
||||||
|
ShelfType.book => BookHomeShelf(
|
||||||
|
title: title,
|
||||||
|
shelf: LibraryItemShelf.fromJson(shelf.toJson()),
|
||||||
|
),
|
||||||
|
ShelfType.authors => AuthorHomeShelf(
|
||||||
|
title: title,
|
||||||
|
shelf: AuthorShelf.fromJson(shelf.toJson()),
|
||||||
|
),
|
||||||
|
_ => Container(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A shelf that displays books on the home page
|
||||||
|
class SimpleHomeShelf extends HookConsumerWidget {
|
||||||
|
const SimpleHomeShelf({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
title,
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
lib/widgets/user_login.dart
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'package:whispering_pages/hacks/fix_autofill_losing_focus.dart';
|
||||||
|
|
||||||
|
class UserLogin extends HookConsumerWidget {
|
||||||
|
UserLogin({
|
||||||
|
super.key,
|
||||||
|
this.usernameController,
|
||||||
|
this.passwordController,
|
||||||
|
this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
TextEditingController? usernameController;
|
||||||
|
TextEditingController? passwordController;
|
||||||
|
final void Function()? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
usernameController ??= useTextEditingController();
|
||||||
|
passwordController ??= useTextEditingController();
|
||||||
|
final isPasswordVisibleAnimationController = useAnimationController(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
|
||||||
|
var isPasswordVisible = useState(false);
|
||||||
|
|
||||||
|
// forward animation when the password visibility changes
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (isPasswordVisible.value) {
|
||||||
|
isPasswordVisibleAnimationController.forward();
|
||||||
|
} else {
|
||||||
|
isPasswordVisibleAnimationController.reverse();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[isPasswordVisible.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: InactiveFocusScopeObserver(
|
||||||
|
child: AutofillGroup(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: usernameController,
|
||||||
|
autofocus: true,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Username',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onBackground
|
||||||
|
.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextFormField(
|
||||||
|
controller: passwordController,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
obscureText: !isPasswordVisible.value,
|
||||||
|
onFieldSubmitted: onPressed != null
|
||||||
|
? (_) {
|
||||||
|
onPressed!();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onBackground
|
||||||
|
.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: ColorFiltered(
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
onTap: () {
|
||||||
|
isPasswordVisible.value = !isPasswordVisible.value;
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||||
|
child: Lottie.asset(
|
||||||
|
'assets/animations/Animation - 1714930099660.json',
|
||||||
|
controller: isPasswordVisibleAnimationController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
maxHeight: 45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: const Text('Login'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
linux/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
flutter/ephemeral
|
||||||
145
linux/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "whispering_pages")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.example.whispering_pages")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||||
|
|
||||||
|
# Root filesystem for cross-building.
|
||||||
|
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||||
|
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration options.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME above,
|
||||||
|
# not the value here, or `flutter run` will no longer work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME}
|
||||||
|
"main.cc"
|
||||||
|
"my_application.cc"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add dependency libraries. Add any application-specific dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
|
# directory.
|
||||||
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Start with a clean build bundle directory every time.
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
88
linux/flutter/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||||
|
# which isn't available in 3.10.
|
||||||
|
function(list_prepend LIST_NAME PREFIX)
|
||||||
|
set(NEW_LIST "")
|
||||||
|
foreach(element ${${LIST_NAME}})
|
||||||
|
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||||
|
endforeach(element)
|
||||||
|
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
15
linux/flutter/generated_plugin_registrant.cc
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
|
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
|
||||||
|
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
|
||||||
|
}
|
||||||
15
linux/flutter/generated_plugin_registrant.h
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
24
linux/flutter/generated_plugins.cmake
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
isar_flutter_libs
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
6
linux/main.cc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
g_autoptr(MyApplication) app = my_application_new();
|
||||||
|
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
|
}
|
||||||
124
linux/my_application.cc
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
struct _MyApplication {
|
||||||
|
GtkApplication parent_instance;
|
||||||
|
char** dart_entrypoint_arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
|
// Implements GApplication::activate.
|
||||||
|
static void my_application_activate(GApplication* application) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
GtkWindow* window =
|
||||||
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
|
// Use a header bar when running in GNOME as this is the common style used
|
||||||
|
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||||
|
// desktop).
|
||||||
|
// If running on X and not using GNOME then just use a traditional title bar
|
||||||
|
// in case the window manager does more exotic layout, e.g. tiling.
|
||||||
|
// If running on Wayland assume the header bar will work (may need changing
|
||||||
|
// if future cases occur).
|
||||||
|
gboolean use_header_bar = TRUE;
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
if (GDK_IS_X11_SCREEN(screen)) {
|
||||||
|
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||||
|
use_header_bar = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (use_header_bar) {
|
||||||
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
|
gtk_header_bar_set_title(header_bar, "whispering_pages");
|
||||||
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
|
} else {
|
||||||
|
gtk_window_set_title(window, "whispering_pages");
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
|
||||||
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||||
|
|
||||||
|
FlView* view = fl_view_new(project);
|
||||||
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::local_command_line.
|
||||||
|
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
// Strip out the first argument as it is the binary name.
|
||||||
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||||
|
|
||||||
|
g_autoptr(GError) error = nullptr;
|
||||||
|
if (!g_application_register(application, nullptr, &error)) {
|
||||||
|
g_warning("Failed to register: %s", error->message);
|
||||||
|
*exit_status = 1;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_application_activate(application);
|
||||||
|
*exit_status = 0;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::startup.
|
||||||
|
static void my_application_startup(GApplication* application) {
|
||||||
|
//MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
|
// Perform any actions required at application startup.
|
||||||
|
|
||||||
|
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::shutdown.
|
||||||
|
static void my_application_shutdown(GApplication* application) {
|
||||||
|
//MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
|
// Perform any actions required at application shutdown.
|
||||||
|
|
||||||
|
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GObject::dispose.
|
||||||
|
static void my_application_dispose(GObject* object) {
|
||||||
|
MyApplication* self = MY_APPLICATION(object);
|
||||||
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
|
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_class_init(MyApplicationClass* klass) {
|
||||||
|
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||||
|
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||||
|
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||||
|
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_init(MyApplication* self) {}
|
||||||
|
|
||||||
|
MyApplication* my_application_new() {
|
||||||
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
|
"application-id", APPLICATION_ID,
|
||||||
|
"flags", G_APPLICATION_NON_UNIQUE,
|
||||||
|
nullptr));
|
||||||
|
}
|
||||||
18
linux/my_application.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||||
|
#define FLUTTER_MY_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||||
|
GtkApplication)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* my_application_new:
|
||||||
|
*
|
||||||
|
* Creates a new Flutter-based application.
|
||||||
|
*
|
||||||
|
* Returns: a new #MyApplication.
|
||||||
|
*/
|
||||||
|
MyApplication* my_application_new();
|
||||||
|
|
||||||
|
#endif // FLUTTER_MY_APPLICATION_H_
|
||||||
1012
pubspec.lock
Normal file
107
pubspec.yaml
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
name: whispering_pages
|
||||||
|
description: "A new Flutter project."
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.3.4 <4.0.0"
|
||||||
|
|
||||||
|
isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
animated_list_plus: ^0.5.2
|
||||||
|
animated_theme_switcher: ^2.0.10
|
||||||
|
auto_scroll_text: ^0.0.7
|
||||||
|
cached_network_image: ^3.3.1
|
||||||
|
coast: ^2.0.2
|
||||||
|
collection: ^1.18.0
|
||||||
|
cupertino_icons: ^1.0.6
|
||||||
|
easy_stepper: ^0.8.4
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_cache_manager: ^3.3.2
|
||||||
|
flutter_hooks: ^0.20.5
|
||||||
|
flutter_settings_ui: ^3.0.1
|
||||||
|
freezed_annotation: ^2.4.1
|
||||||
|
hive: ^4.0.0-dev.2
|
||||||
|
hooks_riverpod: ^2.5.1
|
||||||
|
isar: *isar_version
|
||||||
|
isar_flutter_libs: *isar_version # contains Isar Core
|
||||||
|
json_annotation: ^4.9.0
|
||||||
|
lottie: ^3.1.0
|
||||||
|
path: ^1.9.0
|
||||||
|
path_provider: ^2.1.0
|
||||||
|
riverpod_annotation: ^2.3.5
|
||||||
|
scroll_loop_auto_scroll: ^0.0.5
|
||||||
|
shelfsdk:
|
||||||
|
path: ../../_dart/shelfsdk
|
||||||
|
dev_dependencies:
|
||||||
|
build_runner: ^2.4.9
|
||||||
|
custom_lint: ^0.6.4
|
||||||
|
flutter_lints: ^3.0.0
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
freezed: ^2.5.2
|
||||||
|
json_serializable: ^6.8.0
|
||||||
|
riverpod_generator: ^2.4.0
|
||||||
|
riverpod_lint: ^2.3.10
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
assets:
|
||||||
|
- assets/
|
||||||
|
- assets/animations/
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
||||||
BIN
web/favicon.png
Normal file
|
After Width: | Height: | Size: 917 B |
BIN
web/icons/Icon-192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web/icons/Icon-512.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
web/icons/Icon-maskable-192.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
web/icons/Icon-maskable-512.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
59
web/index.html
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
|
it to work correctly.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
|
the `--base-href` argument provided to `flutter build`.
|
||||||
|
-->
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="whispering_pages">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>whispering_pages</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The value below is injected by flutter build, do not touch.
|
||||||
|
const serviceWorkerVersion = null;
|
||||||
|
</script>
|
||||||
|
<!-- This script adds the flutter initialization JS code -->
|
||||||
|
<script src="flutter.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function(ev) {
|
||||||
|
// Download main.dart.js
|
||||||
|
_flutter.loader.loadEntrypoint({
|
||||||
|
serviceWorker: {
|
||||||
|
serviceWorkerVersion: serviceWorkerVersion,
|
||||||
|
},
|
||||||
|
onEntrypointLoaded: function(engineInitializer) {
|
||||||
|
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||||
|
appRunner.runApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
web/manifest.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "whispering_pages",
|
||||||
|
"short_name": "whispering_pages",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0175C2",
|
||||||
|
"theme_color": "#0175C2",
|
||||||
|
"description": "A new Flutter project.",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
windows/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
108
windows/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(whispering_pages LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "whispering_pages")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(VERSION 3.14...3.25)
|
||||||
|
|
||||||
|
# Define build configuration option.
|
||||||
|
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
if(IS_MULTICONFIG)
|
||||||
|
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
# Define settings for the Profile build mode.
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
|
||||||
|
# Use Unicode for all projects.
|
||||||
|
add_definitions(-DUNICODE -D_UNICODE)
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||||
|
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# Support files are copied into place next to the executable, so that it can
|
||||||
|
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||||
|
# so that building and running from within Visual Studio will work.
|
||||||
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
|
# Make the "install" step default, as it's required to run.
|
||||||
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
CONFIGURATIONS Profile;Release
|
||||||
|
COMPONENT Runtime)
|
||||||
109
windows/flutter/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# Set fallback configurations for older versions of the flutter tool.
|
||||||
|
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"flutter_export.h"
|
||||||
|
"flutter_windows.h"
|
||||||
|
"flutter_messenger.h"
|
||||||
|
"flutter_plugin_registrar.h"
|
||||||
|
"flutter_texture_registrar.h"
|
||||||
|
)
|
||||||
|
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Wrapper ===
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||||
|
"core_implementations.cc"
|
||||||
|
"standard_codec.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||||
|
"plugin_registrar.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||||
|
"flutter_engine.cc"
|
||||||
|
"flutter_view_controller.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
|
||||||
|
# Wrapper sources needed for a plugin.
|
||||||
|
add_library(flutter_wrapper_plugin STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_plugin)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||||
|
|
||||||
|
# Wrapper sources needed for the runner.
|
||||||
|
add_library(flutter_wrapper_app STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_app)
|
||||||
|
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_app PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||||
|
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
${PHONY_OUTPUT}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||