diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..29a3a50
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..d2765fc
--- /dev/null
+++ b/.metadata
@@ -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'
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..44a2752
--- /dev/null
+++ b/.vscode/launch.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4391cf5..3570fc3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,5 +3,10 @@
"activityBar.background": "#5A1021",
"titleBar.activeBackground": "#7E162E",
"titleBar.activeForeground": "#FEFBFC"
- }
+ },
+ "files.exclude": {
+ "**/*.freezed.dart": true,
+ "**/*.g.dart": true
+ },
+ "cSpell.words": ["Autovalidate", "mocktail", "riverpod", "shelfsdk"]
}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..12aa57d
--- /dev/null
+++ b/.vscode/tasks.json
@@ -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
+ }
+ }
+ }
+ ]
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..58cdc4f
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..2c5752a
--- /dev/null
+++ b/analysis_options.yaml
@@ -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
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..6f56801
--- /dev/null
+++ b/android/.gitignore
@@ -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
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..08117bb
--- /dev/null
+++ b/android/app/build.gradle
@@ -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 {}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0557e86
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/example/whispering_pages/MainActivity.kt b/android/app/src/main/kotlin/com/example/whispering_pages/MainActivity.kt
new file mode 100644
index 0000000..f8b7422
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/whispering_pages/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.example.whispering_pages
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity()
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..bc157bd
--- /dev/null
+++ b/android/build.gradle
@@ -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
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..598d13f
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx4G
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e1ca574
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..1d6d19b
--- /dev/null
+++ b/android/settings.gradle
@@ -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"
diff --git a/assets/animations/Animation - 1714930099660.json b/assets/animations/Animation - 1714930099660.json
new file mode 100644
index 0000000..0246241
--- /dev/null
+++ b/assets/animations/Animation - 1714930099660.json
@@ -0,0 +1,1172 @@
+{
+ "v": "5.5.7",
+ "meta": {
+ "g": "LottieFiles AE 0.1.21",
+ "a": "",
+ "k": "",
+ "d": "",
+ "tc": "#ffffff"
+ },
+ "fr": 30,
+ "ip": 0,
+ "op": 60,
+ "w": 1080,
+ "h": 1080,
+ "nm": "occhio",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "ciglia 5 Outlines",
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 43,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [41.335, -49.261],
+ [-41.334, 49.261]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 49,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [64.67, -78.816],
+ [-41.334, 49.261]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "t": 55,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [41.335, -49.261],
+ [-41.334, 49.261]
+ ],
+ "c": false
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 2,
+ "lj": 1,
+ "ml": 10,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [853.762, 386.584], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": [0.39], "y": [1] },
+ "o": { "x": [0.167], "y": [0] },
+ "t": 41,
+ "s": [100]
+ },
+ { "t": 52, "s": [0] }
+ ],
+ "ix": 1
+ },
+ "e": { "a": 0, "k": 100, "ix": 2 },
+ "o": { "a": 0, "k": 0, "ix": 3 },
+ "m": 1,
+ "ix": 2,
+ "nm": "Trim Paths 1",
+ "mn": "ADBE Vector Filter - Trim",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "ciglia 4 Outlines",
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 40,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [32.153, -55.69],
+ [-32.153, 55.69]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 46,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [2.99, -5.729],
+ [0, 0]
+ ],
+ "v": [
+ [56.039, -100.05],
+ [-32.153, 55.69]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "t": 52,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [32.153, -55.69],
+ [-32.153, 55.69]
+ ],
+ "c": false
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 2,
+ "lj": 1,
+ "ml": 10,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [707.038, 308.812], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": [0.39], "y": [1] },
+ "o": { "x": [0.167], "y": [0] },
+ "t": 39,
+ "s": [100]
+ },
+ { "t": 50, "s": [0] }
+ ],
+ "ix": 1
+ },
+ "e": { "a": 0, "k": 100, "ix": 2 },
+ "o": { "a": 0, "k": 0, "ix": 3 },
+ "m": 1,
+ "ix": 2,
+ "nm": "Trim Paths 1",
+ "mn": "ADBE Vector Filter - Trim",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 4,
+ "nm": "ciglia 3 Outlines",
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 39,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [552.093, 221.774],
+ [552.093, 350.384]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 45,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 42.87],
+ [0, 0]
+ ],
+ "v": [
+ [552.093, 153.527],
+ [552.093, 350.384]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "t": 51,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [552.093, 221.774],
+ [552.093, 350.384]
+ ],
+ "c": false
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 2,
+ "lj": 1,
+ "ml": 10,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [0, 0], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": [0.39], "y": [1] },
+ "o": { "x": [0.167], "y": [0] },
+ "t": 37,
+ "s": [100]
+ },
+ { "t": 48, "s": [0] }
+ ],
+ "ix": 1
+ },
+ "e": { "a": 0, "k": 100, "ix": 2 },
+ "o": { "a": 0, "k": 0, "ix": 3 },
+ "m": 1,
+ "ix": 2,
+ "nm": "Trim Paths 1",
+ "mn": "ADBE Vector Filter - Trim",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 4,
+ "ty": 4,
+ "nm": "ciglia 2 Outlines",
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 37,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [21.435, 37.126],
+ [0, 0]
+ ],
+ "v": [
+ [-32.152, -55.69],
+ [32.152, 55.69]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "i": { "x": 0.667, "y": 1 },
+ "o": { "x": 0.333, "y": 0 },
+ "t": 43,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [-59.451, -89.813],
+ [32.152, 55.69]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "t": 49,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [21.435, 37.126],
+ [0, 0]
+ ],
+ "v": [
+ [-32.152, -55.69],
+ [32.152, 55.69]
+ ],
+ "c": false
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 2,
+ "lj": 1,
+ "ml": 10,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [397.507, 308.812], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": [0.39], "y": [1] },
+ "o": { "x": [0.167], "y": [0] },
+ "t": 35,
+ "s": [100]
+ },
+ { "t": 46, "s": [0] }
+ ],
+ "ix": 1
+ },
+ "e": { "a": 0, "k": 100, "ix": 2 },
+ "o": { "a": 0, "k": 0, "ix": 3 },
+ "m": 1,
+ "ix": 2,
+ "nm": "Trim Paths 1",
+ "mn": "ADBE Vector Filter - Trim",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 5,
+ "ty": 4,
+ "nm": "ciglia 1 Outlines",
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.39, "y": 1 },
+ "o": { "x": 0.61, "y": 0 },
+ "t": 37,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [-41.334, -49.261],
+ [41.335, 49.261]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "i": { "x": 0.39, "y": 1 },
+ "o": { "x": 0.61, "y": 0 },
+ "t": 43,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [-18.371, -21.894]
+ ],
+ "o": [
+ [27.556, 32.84],
+ [0, 0]
+ ],
+ "v": [
+ [-68.034, -81.3],
+ [41.335, 49.261]
+ ],
+ "c": false
+ }
+ ]
+ },
+ {
+ "t": 49,
+ "s": [
+ {
+ "i": [
+ [0, 0],
+ [0, 0]
+ ],
+ "o": [
+ [0, 0],
+ [0, 0]
+ ],
+ "v": [
+ [-41.334, -49.261],
+ [41.335, 49.261]
+ ],
+ "c": false
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 2,
+ "lj": 1,
+ "ml": 10,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [246.156, 386.584], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": [0.39], "y": [1] },
+ "o": { "x": [0.167], "y": [0] },
+ "t": 33,
+ "s": [100]
+ },
+ { "t": 44, "s": [0] }
+ ],
+ "ix": 1
+ },
+ "e": { "a": 0, "k": 100, "ix": 2 },
+ "o": { "a": 0, "k": 0, "ix": 3 },
+ "m": 1,
+ "ix": 2,
+ "nm": "Trim Paths 1",
+ "mn": "ADBE Vector Filter - Trim",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 6,
+ "ty": 4,
+ "nm": "Shape Outlines 2",
+ "td": 1,
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.39, "y": 1 },
+ "o": { "x": 0.167, "y": 0 },
+ "t": 18,
+ "s": [
+ {
+ "i": [
+ [3.046, 1.332],
+ [216.329, -12.18],
+ [0.747, 1.332],
+ [-242.002, -0.453]
+ ],
+ "o": [
+ [0, 0],
+ [-269.245, 15.16],
+ [0, 0],
+ [238.003, 0.446]
+ ],
+ "v": [
+ [370.851, 0],
+ [19.568, 215.453],
+ [-370.85, 0],
+ [-6.102, 217.862]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "t": 37,
+ "s": [
+ {
+ "i": [
+ [3.046, 1.332],
+ [216.329, -12.18],
+ [0.747, 1.332],
+ [-242.002, -0.453]
+ ],
+ "o": [
+ [0, 0],
+ [-269.245, 15.16],
+ [0, 0],
+ [238.003, 0.446]
+ ],
+ "v": [
+ [370.851, 0],
+ [19.568, 215.453],
+ [-370.85, 0],
+ [3.898, -216.31]
+ ],
+ "c": true
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 1,
+ "lj": 2,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [552.103, 566.68], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 99.039], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": { "a": 0, "k": [1, 1, 1, 1], "ix": 4 },
+ "o": { "a": 0, "k": 100, "ix": 5 },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 7,
+ "ty": 4,
+ "nm": "pupilla Outlines",
+ "tt": 1,
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.39, "y": 1 },
+ "o": { "x": 0.61, "y": 0 },
+ "t": 15,
+ "s": [542, 620, 0],
+ "to": [-0.333, -13.333, 0],
+ "ti": [0.333, 13.333, 0]
+ },
+ { "t": 39, "s": [540, 540, 0] }
+ ],
+ "ix": 2
+ },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [0, -92.229],
+ [92.228, 0],
+ [0, 92.228],
+ [-92.229, 0]
+ ],
+ "o": [
+ [0, 92.228],
+ [-92.229, 0],
+ [0, -92.229],
+ [92.228, 0]
+ ],
+ "v": [
+ [166.994, 0],
+ [0, 166.994],
+ [-166.994, 0],
+ [0, -166.994]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 4 },
+ "o": { "a": 0, "k": 100, "ix": 5 },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [552.103, 574.226], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 8,
+ "ty": 4,
+ "nm": "Shape Outlines",
+ "sr": 1,
+ "ks": {
+ "o": { "a": 0, "k": 100, "ix": 11 },
+ "r": { "a": 0, "k": 0, "ix": 10 },
+ "p": { "a": 0, "k": [540, 540, 0], "ix": 2 },
+ "a": { "a": 0, "k": [540, 540, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": { "x": 0.39, "y": 1 },
+ "o": { "x": 0.167, "y": 0 },
+ "t": 18,
+ "s": [
+ {
+ "i": [
+ [3.046, 1.332],
+ [216.329, -12.18],
+ [0.747, 1.332],
+ [-242.002, -0.453]
+ ],
+ "o": [
+ [0, 0],
+ [-269.245, 15.16],
+ [0, 0],
+ [238.003, 0.446]
+ ],
+ "v": [
+ [370.851, 0],
+ [19.568, 215.453],
+ [-370.85, 0],
+ [-6.102, 217.862]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "t": 37,
+ "s": [
+ {
+ "i": [
+ [3.046, 1.332],
+ [216.329, -12.18],
+ [0.747, 1.332],
+ [-242.002, -0.453]
+ ],
+ "o": [
+ [0, 0],
+ [-269.245, 15.16],
+ [0, 0],
+ [238.003, 0.446]
+ ],
+ "v": [
+ [370.851, 0],
+ [19.568, 215.453],
+ [-370.85, 0],
+ [3.898, -216.31]
+ ],
+ "c": true
+ }
+ ]
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "st",
+ "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 3 },
+ "o": { "a": 0, "k": 100, "ix": 4 },
+ "w": { "a": 0, "k": 26.489, "ix": 5 },
+ "lc": 1,
+ "lj": 2,
+ "bm": 0,
+ "nm": "Stroke 1",
+ "mn": "ADBE Vector Graphic - Stroke",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": { "a": 0, "k": [552.103, 566.68], "ix": 2 },
+ "a": { "a": 0, "k": [0, 0], "ix": 1 },
+ "s": { "a": 0, "k": [100, 99.039], "ix": 3 },
+ "r": { "a": 0, "k": 0, "ix": 6 },
+ "o": { "a": 0, "k": 100, "ix": 7 },
+ "sk": { "a": 0, "k": 0, "ix": 4 },
+ "sa": { "a": 0, "k": 0, "ix": 5 },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/lib/api/api_provider.dart b/lib/api/api_provider.dart
new file mode 100644
index 0000000..8139060
--- /dev/null
+++ b/lib/api/api_provider.dart
@@ -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 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> 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),
+ ];
+ 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 forceRefresh() async {
+ // clear the cache
+ return apiResponseCacheManager.emptyCache();
+ }
+}
+
+/// fetch continue listening audiobooks
+@riverpod
+FutureOr 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!;
+}
diff --git a/lib/api/api_provider.g.dart b/lib/api/api_provider.g.dart
new file mode 100644
index 0000000..cec1ace
--- /dev/null
+++ b/lib/api/api_provider.g.dart
@@ -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 {
+ /// 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? _dependencies = null;
+
+ @override
+ Iterable? get dependencies => _dependencies;
+
+ static const Iterable? _allTransitiveDependencies = null;
+
+ @override
+ Iterable? 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 {
+ /// 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 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 {
+ /// The parameter `baseUrl` of this provider.
+ Uri? get baseUrl;
+}
+
+class _AudiobookshelfApiProviderElement
+ extends AutoDisposeProviderElement
+ 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.internal(
+ authenticatedApi,
+ name: r'authenticatedApiProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$authenticatedApiHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef AuthenticatedApiRef = AutoDisposeProviderRef;
+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> {
+ /// 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? _dependencies = null;
+
+ @override
+ Iterable? get dependencies => _dependencies;
+
+ static const Iterable? _allTransitiveDependencies = null;
+
+ @override
+ Iterable? 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 {
+ /// 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 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 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 {
+ /// The parameter `address` of this provider.
+ String get address;
+}
+
+class _IsServerAliveProviderElement
+ extends AutoDisposeFutureProviderElement 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.internal(
+ fetchContinueListening,
+ name: r'fetchContinueListeningProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$fetchContinueListeningHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef FetchContinueListeningRef
+ = AutoDisposeFutureProviderRef;
+String _$personalizedViewHash() => r'52a89c46ce668238ca11b5394fd1d14c910947f5';
+
+/// fetch the personalized view
+///
+/// Copied from [PersonalizedView].
+@ProviderFor(PersonalizedView)
+final personalizedViewProvider =
+ AutoDisposeStreamNotifierProvider>.internal(
+ PersonalizedView.new,
+ name: r'personalizedViewProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$personalizedViewHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$PersonalizedView = AutoDisposeStreamNotifier>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/api/authenticated_user_provider.dart b/lib/api/authenticated_user_provider.dart
new file mode 100644
index 0000000..157ce21
--- /dev/null
+++ b/lib/api/authenticated_user_provider.dart
@@ -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 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 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,
+ ),
+ );
+ }
+ }
+}
diff --git a/lib/api/authenticated_user_provider.g.dart b/lib/api/authenticated_user_provider.g.dart
new file mode 100644
index 0000000..1ede072
--- /dev/null
+++ b/lib/api/authenticated_user_provider.g.dart
@@ -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>.internal(
+ AuthenticatedUser.new,
+ name: r'authenticatedUserProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$authenticatedUserHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$AuthenticatedUser = AutoDisposeNotifier>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/api/image_provider.dart b/lib/api/image_provider.dart
new file mode 100644
index 0000000..37245d6
--- /dev/null
+++ b/lib/api/image_provider.dart
@@ -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 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);
+ }
+}
diff --git a/lib/api/image_provider.g.dart b/lib/api/image_provider.g.dart
new file mode 100644
index 0000000..25cf5af
--- /dev/null
+++ b/lib/api/image_provider.g.dart
@@ -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 {
+ late final LibraryItem libraryItem;
+
+ Stream build(
+ LibraryItem libraryItem,
+ );
+}
+
+/// See also [CoverImage].
+@ProviderFor(CoverImage)
+const coverImageProvider = CoverImageFamily();
+
+/// See also [CoverImage].
+class CoverImageFamily extends Family> {
+ /// 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? _dependencies = null;
+
+ @override
+ Iterable? get dependencies => _dependencies;
+
+ static const Iterable? _allTransitiveDependencies = null;
+
+ @override
+ Iterable? get allTransitiveDependencies =>
+ _allTransitiveDependencies;
+
+ @override
+ String? get name => r'coverImageProvider';
+}
+
+/// See also [CoverImage].
+class CoverImageProvider
+ extends AutoDisposeStreamNotifierProviderImpl {
+ /// 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 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
+ 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 {
+ /// The parameter `libraryItem` of this provider.
+ LibraryItem get libraryItem;
+}
+
+class _CoverImageProviderElement
+ extends AutoDisposeStreamNotifierProviderElement
+ 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
diff --git a/lib/api/server_provider.dart b/lib/api/server_provider.dart
new file mode 100644
index 0000000..a9b4267
--- /dev/null
+++ b/lib/api/server_provider.dart
@@ -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 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 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();
+ }
+}
diff --git a/lib/api/server_provider.g.dart b/lib/api/server_provider.g.dart
new file mode 100644
index 0000000..69478cc
--- /dev/null
+++ b/lib/api/server_provider.g.dart
@@ -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>.internal(
+ AudiobookShelfServer.new,
+ name: r'audiobookShelfServerProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$audiobookShelfServerHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$AudiobookShelfServer
+ = AutoDisposeNotifier>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/db/available_boxes.dart b/lib/db/available_boxes.dart
new file mode 100644
index 0000000..8b05f83
--- /dev/null
+++ b/lib/db/available_boxes.dart
@@ -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(name: 'userPrefs');
+
+ /// Box for storing [ApiSettings]
+ static final apiSettingsBox = Hive.box(name: 'apiSettings');
+
+ /// stores the a list of [AudiobookShelfServer]
+ static final serverBox =
+ Hive.box(name: 'audiobookShelfServer');
+
+ /// stores the a list of [AuthenticatedUser]
+ static final authenticatedUserBox =
+ Hive.box(name: 'authenticatedUser');
+}
diff --git a/lib/db/cache/image.g.dart b/lib/db/cache/image.g.dart
new file mode 100644
index 0000000..98a75ab
--- /dev/null
+++ b/lib/db/cache/image.g.dart
@@ -0,0 +1,1009 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'schemas/image.dart';
+
+// **************************************************************************
+// _IsarCollectionGenerator
+// **************************************************************************
+
+// coverage:ignore-file
+// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors
+// ignore_for_file: type=lint
+
+extension GetImageCollection on Isar {
+ IsarCollection get images => this.collection();
+}
+
+const ImageSchema = IsarGeneratedSchema(
+ schema: IsarSchema(
+ name: 'Image',
+ idName: 'id',
+ embedded: false,
+ properties: [
+ IsarPropertySchema(
+ name: 'thumbnailPath',
+ type: IsarType.string,
+ ),
+ IsarPropertySchema(
+ name: 'imagePath',
+ type: IsarType.string,
+ ),
+ IsarPropertySchema(
+ name: 'lastSaved',
+ type: IsarType.dateTime,
+ ),
+ ],
+ indexes: [],
+ ),
+ converter: IsarObjectConverter(
+ serialize: serializeImage,
+ deserialize: deserializeImage,
+ deserializeProperty: deserializeImageProp,
+ ),
+ embeddedSchemas: [],
+);
+
+@isarProtected
+int serializeImage(IsarWriter writer, Image object) {
+ {
+ final value = object.thumbnailPath;
+ if (value == null) {
+ IsarCore.writeNull(writer, 1);
+ } else {
+ IsarCore.writeString(writer, 1, value);
+ }
+ }
+ {
+ final value = object.imagePath;
+ if (value == null) {
+ IsarCore.writeNull(writer, 2);
+ } else {
+ IsarCore.writeString(writer, 2, value);
+ }
+ }
+ IsarCore.writeLong(
+ writer, 3, object.lastSaved.toUtc().microsecondsSinceEpoch);
+ return object.id;
+}
+
+@isarProtected
+Image deserializeImage(IsarReader reader) {
+ final int _id;
+ _id = IsarCore.readId(reader);
+ final String? _thumbnailPath;
+ _thumbnailPath = IsarCore.readString(reader, 1);
+ final String? _imagePath;
+ _imagePath = IsarCore.readString(reader, 2);
+ final object = Image(
+ id: _id,
+ thumbnailPath: _thumbnailPath,
+ imagePath: _imagePath,
+ );
+ {
+ final value = IsarCore.readLong(reader, 3);
+ if (value == -9223372036854775808) {
+ object.lastSaved =
+ DateTime.fromMillisecondsSinceEpoch(0, isUtc: true).toLocal();
+ } else {
+ object.lastSaved =
+ DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true).toLocal();
+ }
+ }
+ return object;
+}
+
+@isarProtected
+dynamic deserializeImageProp(IsarReader reader, int property) {
+ switch (property) {
+ case 0:
+ return IsarCore.readId(reader);
+ case 1:
+ return IsarCore.readString(reader, 1);
+ case 2:
+ return IsarCore.readString(reader, 2);
+ case 3:
+ {
+ final value = IsarCore.readLong(reader, 3);
+ if (value == -9223372036854775808) {
+ return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true).toLocal();
+ } else {
+ return DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true)
+ .toLocal();
+ }
+ }
+ default:
+ throw ArgumentError('Unknown property: $property');
+ }
+}
+
+sealed class _ImageUpdate {
+ bool call({
+ required int id,
+ String? thumbnailPath,
+ String? imagePath,
+ DateTime? lastSaved,
+ });
+}
+
+class _ImageUpdateImpl implements _ImageUpdate {
+ const _ImageUpdateImpl(this.collection);
+
+ final IsarCollection collection;
+
+ @override
+ bool call({
+ required int id,
+ Object? thumbnailPath = ignore,
+ Object? imagePath = ignore,
+ Object? lastSaved = ignore,
+ }) {
+ return collection.updateProperties([
+ id
+ ], {
+ if (thumbnailPath != ignore) 1: thumbnailPath as String?,
+ if (imagePath != ignore) 2: imagePath as String?,
+ if (lastSaved != ignore) 3: lastSaved as DateTime?,
+ }) >
+ 0;
+ }
+}
+
+sealed class _ImageUpdateAll {
+ int call({
+ required List id,
+ String? thumbnailPath,
+ String? imagePath,
+ DateTime? lastSaved,
+ });
+}
+
+class _ImageUpdateAllImpl implements _ImageUpdateAll {
+ const _ImageUpdateAllImpl(this.collection);
+
+ final IsarCollection collection;
+
+ @override
+ int call({
+ required List id,
+ Object? thumbnailPath = ignore,
+ Object? imagePath = ignore,
+ Object? lastSaved = ignore,
+ }) {
+ return collection.updateProperties(id, {
+ if (thumbnailPath != ignore) 1: thumbnailPath as String?,
+ if (imagePath != ignore) 2: imagePath as String?,
+ if (lastSaved != ignore) 3: lastSaved as DateTime?,
+ });
+ }
+}
+
+extension ImageUpdate on IsarCollection {
+ _ImageUpdate get update => _ImageUpdateImpl(this);
+
+ _ImageUpdateAll get updateAll => _ImageUpdateAllImpl(this);
+}
+
+sealed class _ImageQueryUpdate {
+ int call({
+ String? thumbnailPath,
+ String? imagePath,
+ DateTime? lastSaved,
+ });
+}
+
+class _ImageQueryUpdateImpl implements _ImageQueryUpdate {
+ const _ImageQueryUpdateImpl(this.query, {this.limit});
+
+ final IsarQuery query;
+ final int? limit;
+
+ @override
+ int call({
+ Object? thumbnailPath = ignore,
+ Object? imagePath = ignore,
+ Object? lastSaved = ignore,
+ }) {
+ return query.updateProperties(limit: limit, {
+ if (thumbnailPath != ignore) 1: thumbnailPath as String?,
+ if (imagePath != ignore) 2: imagePath as String?,
+ if (lastSaved != ignore) 3: lastSaved as DateTime?,
+ });
+ }
+}
+
+extension ImageQueryUpdate on IsarQuery {
+ _ImageQueryUpdate get updateFirst => _ImageQueryUpdateImpl(this, limit: 1);
+
+ _ImageQueryUpdate get updateAll => _ImageQueryUpdateImpl(this);
+}
+
+class _ImageQueryBuilderUpdateImpl implements _ImageQueryUpdate {
+ const _ImageQueryBuilderUpdateImpl(this.query, {this.limit});
+
+ final QueryBuilder query;
+ final int? limit;
+
+ @override
+ int call({
+ Object? thumbnailPath = ignore,
+ Object? imagePath = ignore,
+ Object? lastSaved = ignore,
+ }) {
+ final q = query.build();
+ try {
+ return q.updateProperties(limit: limit, {
+ if (thumbnailPath != ignore) 1: thumbnailPath as String?,
+ if (imagePath != ignore) 2: imagePath as String?,
+ if (lastSaved != ignore) 3: lastSaved as DateTime?,
+ });
+ } finally {
+ q.close();
+ }
+ }
+}
+
+extension ImageQueryBuilderUpdate on QueryBuilder {
+ _ImageQueryUpdate get updateFirst =>
+ _ImageQueryBuilderUpdateImpl(this, limit: 1);
+
+ _ImageQueryUpdate get updateAll => _ImageQueryBuilderUpdateImpl(this);
+}
+
+extension ImageQueryFilter on QueryBuilder {
+ QueryBuilder idEqualTo(
+ int value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ EqualCondition(
+ property: 0,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder idGreaterThan(
+ int value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterCondition(
+ property: 0,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder idGreaterThanOrEqualTo(
+ int value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterOrEqualCondition(
+ property: 0,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder idLessThan(
+ int value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessCondition(
+ property: 0,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder idLessThanOrEqualTo(
+ int value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessOrEqualCondition(
+ property: 0,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder idBetween(
+ int lower,
+ int upper,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ BetweenCondition(
+ property: 0,
+ lower: lower,
+ upper: upper,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const IsNullCondition(property: 1));
+ });
+ }
+
+ QueryBuilder thumbnailPathIsNotNull() {
+ return QueryBuilder.apply(not(), (query) {
+ return query.addFilterCondition(const IsNullCondition(property: 1));
+ });
+ }
+
+ QueryBuilder thumbnailPathEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ EqualCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathGreaterThan(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder
+ thumbnailPathGreaterThanOrEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterOrEqualCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathLessThan(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder
+ thumbnailPathLessThanOrEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessOrEqualCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathBetween(
+ String? lower,
+ String? upper, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ BetweenCondition(
+ property: 1,
+ lower: lower,
+ upper: upper,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathStartsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ StartsWithCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathEndsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ EndsWithCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathContains(
+ String value,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ ContainsCondition(
+ property: 1,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathMatches(
+ String pattern,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ MatchesCondition(
+ property: 1,
+ wildcard: pattern,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathIsEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const EqualCondition(
+ property: 1,
+ value: '',
+ ),
+ );
+ });
+ }
+
+ QueryBuilder thumbnailPathIsNotEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const GreaterCondition(
+ property: 1,
+ value: '',
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const IsNullCondition(property: 2));
+ });
+ }
+
+ QueryBuilder imagePathIsNotNull() {
+ return QueryBuilder.apply(not(), (query) {
+ return query.addFilterCondition(const IsNullCondition(property: 2));
+ });
+ }
+
+ QueryBuilder imagePathEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ EqualCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathGreaterThan(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder
+ imagePathGreaterThanOrEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterOrEqualCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathLessThan(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathLessThanOrEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessOrEqualCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathBetween(
+ String? lower,
+ String? upper, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ BetweenCondition(
+ property: 2,
+ lower: lower,
+ upper: upper,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathStartsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ StartsWithCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathEndsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ EndsWithCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathContains(
+ String value,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ ContainsCondition(
+ property: 2,
+ value: value,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathMatches(
+ String pattern,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ MatchesCondition(
+ property: 2,
+ wildcard: pattern,
+ caseSensitive: caseSensitive,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathIsEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const EqualCondition(
+ property: 2,
+ value: '',
+ ),
+ );
+ });
+ }
+
+ QueryBuilder imagePathIsNotEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ const GreaterCondition(
+ property: 2,
+ value: '',
+ ),
+ );
+ });
+ }
+
+ QueryBuilder lastSavedEqualTo(
+ DateTime value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ EqualCondition(
+ property: 3,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder lastSavedGreaterThan(
+ DateTime value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterCondition(
+ property: 3,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder
+ lastSavedGreaterThanOrEqualTo(
+ DateTime value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ GreaterOrEqualCondition(
+ property: 3,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder lastSavedLessThan(
+ DateTime value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessCondition(
+ property: 3,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder lastSavedLessThanOrEqualTo(
+ DateTime value,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ LessOrEqualCondition(
+ property: 3,
+ value: value,
+ ),
+ );
+ });
+ }
+
+ QueryBuilder lastSavedBetween(
+ DateTime lower,
+ DateTime upper,
+ ) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(
+ BetweenCondition(
+ property: 3,
+ lower: lower,
+ upper: upper,
+ ),
+ );
+ });
+ }
+}
+
+extension ImageQueryObject on QueryBuilder {}
+
+extension ImageQuerySortBy on QueryBuilder {
+ QueryBuilder sortById() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(0);
+ });
+ }
+
+ QueryBuilder sortByIdDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(0, sort: Sort.desc);
+ });
+ }
+
+ QueryBuilder sortByThumbnailPath(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(
+ 1,
+ caseSensitive: caseSensitive,
+ );
+ });
+ }
+
+ QueryBuilder sortByThumbnailPathDesc(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(
+ 1,
+ sort: Sort.desc,
+ caseSensitive: caseSensitive,
+ );
+ });
+ }
+
+ QueryBuilder sortByImagePath(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(
+ 2,
+ caseSensitive: caseSensitive,
+ );
+ });
+ }
+
+ QueryBuilder sortByImagePathDesc(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(
+ 2,
+ sort: Sort.desc,
+ caseSensitive: caseSensitive,
+ );
+ });
+ }
+
+ QueryBuilder sortByLastSaved() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(3);
+ });
+ }
+
+ QueryBuilder sortByLastSavedDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(3, sort: Sort.desc);
+ });
+ }
+}
+
+extension ImageQuerySortThenBy on QueryBuilder {
+ QueryBuilder thenById() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(0);
+ });
+ }
+
+ QueryBuilder thenByIdDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(0, sort: Sort.desc);
+ });
+ }
+
+ QueryBuilder thenByThumbnailPath(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(1, caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder thenByThumbnailPathDesc(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(1, sort: Sort.desc, caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder thenByImagePath(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(2, caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder thenByImagePathDesc(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(2, sort: Sort.desc, caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder thenByLastSaved() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(3);
+ });
+ }
+
+ QueryBuilder thenByLastSavedDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(3, sort: Sort.desc);
+ });
+ }
+}
+
+extension ImageQueryWhereDistinct on QueryBuilder {
+ QueryBuilder distinctByThumbnailPath(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(1, caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder distinctByImagePath(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(2, caseSensitive: caseSensitive);
+ });
+ }
+
+ QueryBuilder distinctByLastSaved() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(3);
+ });
+ }
+}
+
+extension ImageQueryProperty1 on QueryBuilder {
+ QueryBuilder idProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(0);
+ });
+ }
+
+ QueryBuilder thumbnailPathProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(1);
+ });
+ }
+
+ QueryBuilder imagePathProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(2);
+ });
+ }
+
+ QueryBuilder lastSavedProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(3);
+ });
+ }
+}
+
+extension ImageQueryProperty2 on QueryBuilder {
+ QueryBuilder idProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(0);
+ });
+ }
+
+ QueryBuilder thumbnailPathProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(1);
+ });
+ }
+
+ QueryBuilder imagePathProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(2);
+ });
+ }
+
+ QueryBuilder lastSavedProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(3);
+ });
+ }
+}
+
+extension ImageQueryProperty3
+ on QueryBuilder {
+ QueryBuilder idProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(0);
+ });
+ }
+
+ QueryBuilder thumbnailPathProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(1);
+ });
+ }
+
+ QueryBuilder imagePathProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(2);
+ });
+ }
+
+ QueryBuilder lastSavedProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addProperty(3);
+ });
+ }
+}
diff --git a/lib/db/cache/schemas/image.dart b/lib/db/cache/schemas/image.dart
new file mode 100644
index 0000000..8ebd982
--- /dev/null
+++ b/lib/db/cache/schemas/image.dart
@@ -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();
+ }
+}
diff --git a/lib/db/cache_manager.dart b/lib/db/cache_manager.dart
new file mode 100644
index 0000000..8317560
--- /dev/null
+++ b/lib/db/cache_manager.dart
@@ -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,
+ ),
+);
diff --git a/lib/db/init.dart b/lib/db/init.dart
new file mode 100644
index 0000000..672a2e9
--- /dev/null
+++ b/lib/db/init.dart
@@ -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();
+}
diff --git a/lib/db/register_models.dart b/lib/db/register_models.dart
new file mode 100644
index 0000000..31857fc
--- /dev/null
+++ b/lib/db/register_models.dart
@@ -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',
+ ((json) => AppSettings.fromJson(json)),
+ );
+ Hive.registerAdapter(
+ 'ApiSettings',
+ ((json) => ApiSettings.fromJson(json)),
+ );
+ Hive.registerAdapter(
+ 'AudiobookShelfServer',
+ ((json) => AudiobookShelfServer.fromJson(json)),
+ );
+ Hive.registerAdapter(
+ 'AuthenticatedUser',
+ ((json) => AuthenticatedUser.fromJson(json)),
+ );
+}
diff --git a/lib/db/storage.dart b/lib/db/storage.dart
new file mode 100644
index 0000000..5042c81
--- /dev/null
+++ b/lib/db/storage.dart
@@ -0,0 +1,3 @@
+export 'available_boxes.dart';
+export 'init.dart';
+export 'register_models.dart';
diff --git a/lib/hacks/fix_autofill_losing_focus.dart b/lib/hacks/fix_autofill_losing_focus.dart
new file mode 100644
index 0000000..ffa7da6
--- /dev/null
+++ b/lib/hacks/fix_autofill_losing_focus.dart
@@ -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 createState() =>
+ _InactiveFocusScopeObserverState();
+}
+
+class _InactiveFocusScopeObserverState
+ extends State {
+ 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;
+ }
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..7743a22
--- /dev/null
+++ b/lib/main.dart
@@ -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(),
+ );
+ }
+}
diff --git a/lib/pages/app_settings.dart b/lib/pages/app_settings.dart
new file mode 100644
index 0000000..759df6b
--- /dev/null
+++ b/lib/pages/app_settings.dart
@@ -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();
+
+ 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();
+ },
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart
new file mode 100644
index 0000000..2a83101
--- /dev/null
+++ b/lib/pages/home_page.dart
@@ -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');
+ },
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/onboarding/onboarding.dart b/lib/pages/onboarding/onboarding.dart
new file mode 100644
index 0000000..7971df1
--- /dev/null
+++ b/lib/pages/onboarding/onboarding.dart
@@ -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 {
+ 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(),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/onboarding/server_setup.dart b/lib/pages/onboarding/server_setup.dart
new file mode 100644
index 0000000..cbd7c6c
--- /dev/null
+++ b/lib/pages/onboarding/server_setup.dart
@@ -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,
+ ),
+ );
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/onboarding/user_login.dart b/lib/pages/onboarding/user_login.dart
new file mode 100644
index 0000000..32dba03
--- /dev/null
+++ b/lib/pages/onboarding/user_login.dart
@@ -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,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/pages.dart b/lib/pages/pages.dart
new file mode 100644
index 0000000..fa05138
--- /dev/null
+++ b/lib/pages/pages.dart
@@ -0,0 +1,2 @@
+export 'home_page.dart';
+export 'server_manager.dart';
diff --git a/lib/pages/server_manager.dart b/lib/pages/server_manager.dart
new file mode 100644
index 0000000..f33a457
--- /dev/null
+++ b/lib/pages/server_manager.dart
@@ -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();
+
+ 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'),
+ ),
+ );
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/settings/api_settings_provider.dart b/lib/settings/api_settings_provider.dart
new file mode 100644
index 0000000..24d54f6
--- /dev/null
+++ b/lib/settings/api_settings_provider.dart
@@ -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;
+ }
+}
diff --git a/lib/settings/api_settings_provider.g.dart b/lib/settings/api_settings_provider.g.dart
new file mode 100644
index 0000000..39047bc
--- /dev/null
+++ b/lib/settings/api_settings_provider.g.dart
@@ -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.internal(
+ ApiSettings.new,
+ name: r'apiSettingsProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$ApiSettings = AutoDisposeNotifier;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/settings/app_settings_provider.dart b/lib/settings/app_settings_provider.dart
new file mode 100644
index 0000000..ccfe9df
--- /dev/null
+++ b/lib/settings/app_settings_provider.dart
@@ -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;
+ }
+}
diff --git a/lib/settings/app_settings_provider.g.dart b/lib/settings/app_settings_provider.g.dart
new file mode 100644
index 0000000..b8de14e
--- /dev/null
+++ b/lib/settings/app_settings_provider.g.dart
@@ -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.internal(
+ AppSettings.new,
+ name: r'appSettingsProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$AppSettings = AutoDisposeNotifier;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/settings/constants.dart b/lib/settings/constants.dart
new file mode 100644
index 0000000..3edcd07
--- /dev/null
+++ b/lib/settings/constants.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/foundation.dart' show immutable;
+
+
+@immutable
+class AppMetadata {
+ const AppMetadata._();
+ static const String appName = 'Whispering Pages';
+}
diff --git a/lib/settings/models/api_settings.dart b/lib/settings/models/api_settings.dart
new file mode 100644
index 0000000..6b1be19
--- /dev/null
+++ b/lib/settings/models/api_settings.dart
@@ -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 json) =>
+ _$ApiSettingsFromJson(json);
+}
diff --git a/lib/settings/models/api_settings.freezed.dart b/lib/settings/models/api_settings.freezed.dart
new file mode 100644
index 0000000..ca20663
--- /dev/null
+++ b/lib/settings/models/api_settings.freezed.dart
@@ -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 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 json) {
+ return _ApiSettings.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ApiSettings {
+ AudiobookShelfServer? get activeServer => throw _privateConstructorUsedError;
+ AuthenticatedUser? get activeUser => throw _privateConstructorUsedError;
+ String? get activeLibraryId => throw _privateConstructorUsedError;
+
+ Map toJson() => throw _privateConstructorUsedError;
+ @JsonKey(ignore: true)
+ $ApiSettingsCopyWith 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 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 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 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;
+}
diff --git a/lib/settings/models/api_settings.g.dart b/lib/settings/models/api_settings.g.dart
new file mode 100644
index 0000000..568754a
--- /dev/null
+++ b/lib/settings/models/api_settings.g.dart
@@ -0,0 +1,27 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'api_settings.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map json) =>
+ _$ApiSettingsImpl(
+ activeServer: json['activeServer'] == null
+ ? null
+ : AudiobookShelfServer.fromJson(
+ json['activeServer'] as Map),
+ activeUser: json['activeUser'] == null
+ ? null
+ : AuthenticatedUser.fromJson(
+ json['activeUser'] as Map),
+ activeLibraryId: json['activeLibraryId'] as String?,
+ );
+
+Map _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) =>
+ {
+ 'activeServer': instance.activeServer,
+ 'activeUser': instance.activeUser,
+ 'activeLibraryId': instance.activeLibraryId,
+ };
diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart
new file mode 100644
index 0000000..9d7172e
--- /dev/null
+++ b/lib/settings/models/app_settings.dart
@@ -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 json) =>
+ _$AppSettingsFromJson(json);
+}
diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart
new file mode 100644
index 0000000..395d7f2
--- /dev/null
+++ b/lib/settings/models/app_settings.freezed.dart
@@ -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 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 json) {
+ return _AppSettings.fromJson(json);
+}
+
+/// @nodoc
+mixin _$AppSettings {
+ bool get isDarkMode => throw _privateConstructorUsedError;
+
+ Map toJson() => throw _privateConstructorUsedError;
+ @JsonKey(ignore: true)
+ $AppSettingsCopyWith 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 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 toJson() {
+ return _$$AppSettingsImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _AppSettings implements AppSettings {
+ const factory _AppSettings({final bool isDarkMode}) = _$AppSettingsImpl;
+
+ factory _AppSettings.fromJson(Map json) =
+ _$AppSettingsImpl.fromJson;
+
+ @override
+ bool get isDarkMode;
+ @override
+ @JsonKey(ignore: true)
+ _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart
new file mode 100644
index 0000000..94b8c3a
--- /dev/null
+++ b/lib/settings/models/app_settings.g.dart
@@ -0,0 +1,17 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'app_settings.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) =>
+ _$AppSettingsImpl(
+ isDarkMode: json['isDarkMode'] as bool? ?? true,
+ );
+
+Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
+ {
+ 'isDarkMode': instance.isDarkMode,
+ };
diff --git a/lib/settings/models/audiobookshelf_server.dart b/lib/settings/models/audiobookshelf_server.dart
new file mode 100644
index 0000000..0c97e41
--- /dev/null
+++ b/lib/settings/models/audiobookshelf_server.dart
@@ -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 json) =>
+ _$AudiobookShelfServerFromJson(json);
+}
diff --git a/lib/settings/models/audiobookshelf_server.freezed.dart b/lib/settings/models/audiobookshelf_server.freezed.dart
new file mode 100644
index 0000000..8d68178
--- /dev/null
+++ b/lib/settings/models/audiobookshelf_server.freezed.dart
@@ -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 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 json) {
+ return _AudiobookShelfServer.fromJson(json);
+}
+
+/// @nodoc
+mixin _$AudiobookShelfServer {
+ Uri get serverUrl => throw _privateConstructorUsedError;
+
+ Map toJson() => throw _privateConstructorUsedError;
+ @JsonKey(ignore: true)
+ $AudiobookShelfServerCopyWith 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 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 toJson() {
+ return _$$AudiobookShelfServerImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _AudiobookShelfServer implements AudiobookShelfServer {
+ const factory _AudiobookShelfServer({required final Uri serverUrl}) =
+ _$AudiobookShelfServerImpl;
+
+ factory _AudiobookShelfServer.fromJson(Map json) =
+ _$AudiobookShelfServerImpl.fromJson;
+
+ @override
+ Uri get serverUrl;
+ @override
+ @JsonKey(ignore: true)
+ _$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
+ get copyWith => throw _privateConstructorUsedError;
+}
diff --git a/lib/settings/models/audiobookshelf_server.g.dart b/lib/settings/models/audiobookshelf_server.g.dart
new file mode 100644
index 0000000..a876683
--- /dev/null
+++ b/lib/settings/models/audiobookshelf_server.g.dart
@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'audiobookshelf_server.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson(
+ Map json) =>
+ _$AudiobookShelfServerImpl(
+ serverUrl: Uri.parse(json['serverUrl'] as String),
+ );
+
+Map _$$AudiobookShelfServerImplToJson(
+ _$AudiobookShelfServerImpl instance) =>
+ {
+ 'serverUrl': instance.serverUrl.toString(),
+ };
diff --git a/lib/settings/models/authenticated_user.dart b/lib/settings/models/authenticated_user.dart
new file mode 100644
index 0000000..be330e9
--- /dev/null
+++ b/lib/settings/models/authenticated_user.dart
@@ -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 json) =>
+ _$AuthenticatedUserFromJson(json);
+}
diff --git a/lib/settings/models/authenticated_user.freezed.dart b/lib/settings/models/authenticated_user.freezed.dart
new file mode 100644
index 0000000..9859db6
--- /dev/null
+++ b/lib/settings/models/authenticated_user.freezed.dart
@@ -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 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 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 toJson() => throw _privateConstructorUsedError;
+ @JsonKey(ignore: true)
+ $AuthenticatedUserCopyWith 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 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 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 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;
+}
diff --git a/lib/settings/models/authenticated_user.g.dart b/lib/settings/models/authenticated_user.g.dart
new file mode 100644
index 0000000..0752807
--- /dev/null
+++ b/lib/settings/models/authenticated_user.g.dart
@@ -0,0 +1,28 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'authenticated_user.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
+ Map json) =>
+ _$AuthenticatedUserImpl(
+ server:
+ AudiobookShelfServer.fromJson(json['server'] as Map),
+ authToken: json['authToken'] as String,
+ id: json['id'] as String?,
+ username: json['username'] as String?,
+ password: json['password'] as String?,
+ );
+
+Map _$$AuthenticatedUserImplToJson(
+ _$AuthenticatedUserImpl instance) =>
+ {
+ 'server': instance.server,
+ 'authToken': instance.authToken,
+ 'id': instance.id,
+ 'username': instance.username,
+ 'password': instance.password,
+ };
diff --git a/lib/settings/models/models.dart b/lib/settings/models/models.dart
new file mode 100644
index 0000000..6fc13da
--- /dev/null
+++ b/lib/settings/models/models.dart
@@ -0,0 +1,4 @@
+export 'api_settings.dart';
+export 'app_settings.dart';
+export 'audiobookshelf_server.dart';
+export 'authenticated_user.dart';
diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart
new file mode 100644
index 0000000..2eeef42
--- /dev/null
+++ b/lib/settings/settings.dart
@@ -0,0 +1,2 @@
+export 'app_settings_provider.dart';
+export 'constants.dart';
diff --git a/lib/theme/dark.dart b/lib/theme/dark.dart
new file mode 100644
index 0000000..aba3db5
--- /dev/null
+++ b/lib/theme/dark.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/material.dart';
+
+final ThemeData darkTheme = ThemeData(
+ brightness: Brightness.dark,
+ colorScheme: ColorScheme.dark(
+ background: Colors.grey[900]!,
+ ),
+);
diff --git a/lib/theme/light.dart b/lib/theme/light.dart
new file mode 100644
index 0000000..e401c23
--- /dev/null
+++ b/lib/theme/light.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/material.dart';
+
+final ThemeData lightTheme = ThemeData(
+ brightness: Brightness.light,
+ colorScheme: ColorScheme.light(
+ background: Colors.grey[200]!,
+ ),
+);
diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart
new file mode 100644
index 0000000..8c777fc
--- /dev/null
+++ b/lib/theme/theme.dart
@@ -0,0 +1,2 @@
+export 'dark.dart';
+export 'light.dart';
diff --git a/lib/widgets/add_new_server.dart b/lib/widgets/add_new_server.dart
new file mode 100644
index 0000000..652a2d6
--- /dev/null
+++ b/lib/widgets/add_new_server.dart
@@ -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
+ }
+}
diff --git a/lib/widgets/drawer.dart b/lib/widgets/drawer.dart
new file mode 100644
index 0000000..922aba6
--- /dev/null
+++ b/lib/widgets/drawer.dart
@@ -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(),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/shelves/author_shelf.dart b/lib/widgets/shelves/author_shelf.dart
new file mode 100644
index 0000000..0c48ebc
--- /dev/null
+++ b/lib/widgets/shelves/author_shelf.dart
@@ -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,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/shelves/book_shelf.dart b/lib/widgets/shelves/book_shelf.dart
new file mode 100644
index 0000000..3a65184
--- /dev/null
+++ b/lib/widgets/shelves/book_shelf.dart
@@ -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,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/shelves/home_shelf.dart b/lib/widgets/shelves/home_shelf.dart
new file mode 100644
index 0000000..c881f33
--- /dev/null
+++ b/lib/widgets/shelves/home_shelf.dart
@@ -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 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,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/user_login.dart b/lib/widgets/user_login.dart
new file mode 100644
index 0000000..1990629
--- /dev/null
+++ b/lib/widgets/user_login.dart
@@ -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'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/linux/.gitignore b/linux/.gitignore
new file mode 100644
index 0000000..d3896c9
--- /dev/null
+++ b/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
new file mode 100644
index 0000000..a222344
--- /dev/null
+++ b/linux/CMakeLists.txt
@@ -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 "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>: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()
diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000..d5bd016
--- /dev/null
+++ b/linux/flutter/CMakeLists.txt
@@ -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}
+)
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..b898c8c
--- /dev/null
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+
+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);
+}
diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..e0f0a47
--- /dev/null
+++ b/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..cb083af
--- /dev/null
+++ b/linux/flutter/generated_plugins.cmake
@@ -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 $)
+ 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)
diff --git a/linux/main.cc b/linux/main.cc
new file mode 100644
index 0000000..e7c5c54
--- /dev/null
+++ b/linux/main.cc
@@ -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);
+}
diff --git a/linux/my_application.cc b/linux/my_application.cc
new file mode 100644
index 0000000..0d202a2
--- /dev/null
+++ b/linux/my_application.cc
@@ -0,0 +1,124 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#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));
+}
diff --git a/linux/my_application.h b/linux/my_application.h
new file mode 100644
index 0000000..72271d5
--- /dev/null
+++ b/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+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_
diff --git a/pubspec.lock b/pubspec.lock
new file mode 100644
index 0000000..15c1a8f
--- /dev/null
+++ b/pubspec.lock
@@ -0,0 +1,1012 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "67.0.0"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.4.1"
+ analyzer_plugin:
+ dependency: transitive
+ description:
+ name: analyzer_plugin
+ sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.3"
+ animated_list_plus:
+ dependency: "direct main"
+ description:
+ name: animated_list_plus
+ sha256: fb3d7f1fbaf5af84907f3c739236bacda8bf32cbe1f118dd51510752883ff50c
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.2"
+ animated_theme_switcher:
+ dependency: "direct main"
+ description:
+ name: animated_theme_switcher
+ sha256: "24ccd74437b8db78f6d1ec701804702817bced5f925b1b3419c7a93071e3d3e9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.10"
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.5.0"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.0"
+ auto_scroll_text:
+ dependency: "direct main"
+ description:
+ name: auto_scroll_text
+ sha256: "8de28056f844f24f13771606417ffa109397f75a66440fe60ec2d38c133e16dc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.7"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ build:
+ dependency: transitive
+ description:
+ name: build
+ sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ build_config:
+ dependency: transitive
+ description:
+ name: build_config
+ sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ build_daemon:
+ dependency: transitive
+ description:
+ name: build_daemon
+ sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.1"
+ build_resolvers:
+ dependency: transitive
+ description:
+ name: build_resolvers
+ sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.2"
+ build_runner:
+ dependency: "direct dev"
+ description:
+ name: build_runner
+ sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.9"
+ build_runner_core:
+ dependency: transitive
+ description:
+ name: build_runner_core
+ sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.3.0"
+ built_collection:
+ dependency: transitive
+ description:
+ name: built_collection
+ sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
+ built_value:
+ dependency: transitive
+ description:
+ name: built_value
+ sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.9.2"
+ cached_network_image:
+ dependency: "direct main"
+ description:
+ name: cached_network_image
+ sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.3.1"
+ cached_network_image_platform_interface:
+ dependency: transitive
+ description:
+ name: cached_network_image_platform_interface
+ sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ cached_network_image_web:
+ dependency: transitive
+ description:
+ name: cached_network_image_web
+ sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.3"
+ ci:
+ dependency: transitive
+ description:
+ name: ci
+ sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.0"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ coast:
+ dependency: "direct main"
+ description:
+ name: coast
+ sha256: a85fdf09a387ea511ce790a79a11e673b8ee05e1ac142e17e0fde666a8fda853
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.2"
+ code_builder:
+ dependency: transitive
+ description:
+ name: code_builder
+ sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.10.0"
+ collection:
+ dependency: "direct main"
+ description:
+ name: collection
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.18.0"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.8"
+ custom_lint:
+ dependency: "direct dev"
+ description:
+ name: custom_lint
+ sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.4"
+ custom_lint_builder:
+ dependency: transitive
+ description:
+ name: custom_lint_builder
+ sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.4"
+ custom_lint_core:
+ dependency: transitive
+ description:
+ name: custom_lint_core
+ sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.3"
+ dart_style:
+ dependency: transitive
+ description:
+ name: dart_style
+ sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.6"
+ easy_stepper:
+ dependency: "direct main"
+ description:
+ name: easy_stepper
+ sha256: "8d1d9f048f6a079dcfa98cea56650d77976f21a50033bf99dd0d82046e12060b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.4"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.1"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_cache_manager:
+ dependency: "direct main"
+ description:
+ name: flutter_cache_manager
+ sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.3.2"
+ flutter_hooks:
+ dependency: "direct main"
+ description:
+ name: flutter_hooks
+ sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.20.5"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ flutter_riverpod:
+ dependency: transitive
+ description:
+ name: flutter_riverpod
+ sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.1"
+ flutter_settings_ui:
+ dependency: "direct main"
+ description:
+ name: flutter_settings_ui
+ sha256: dcc506fab724192594e5c232b6214a941abd6e7b5151626635b89258fadbc17c
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ freezed:
+ dependency: "direct dev"
+ description:
+ name: freezed
+ sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.2"
+ freezed_annotation:
+ dependency: "direct main"
+ description:
+ name: freezed_annotation
+ sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ frontend_server_client:
+ dependency: transitive
+ description:
+ name: frontend_server_client
+ sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ graphs:
+ dependency: transitive
+ description:
+ name: graphs
+ sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.1"
+ hive:
+ dependency: "direct main"
+ description:
+ name: hive
+ sha256: "10819524df282842ebae12870e2e0e9ebc3e5c4637bec741ad39b919c589cb20"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0-dev.2"
+ hooks_riverpod:
+ dependency: "direct main"
+ description:
+ name: hooks_riverpod
+ sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.1"
+ hotreloader:
+ dependency: transitive
+ description:
+ name: hotreloader
+ sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ isar:
+ dependency: "direct main"
+ description:
+ name: isar
+ sha256: ebf74d87c400bd9f7da14acb31932b50c2407edbbd40930da3a6c2a8143f85a8
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0-dev.14"
+ isar_flutter_libs:
+ dependency: "direct main"
+ description:
+ name: isar_flutter_libs
+ sha256: "04a3f4035e213ddb6e78d0132a7c80296a085c2088c2a761b4a42ee5add36983"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0-dev.14"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.7"
+ json_annotation:
+ dependency: "direct main"
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
+ json_serializable:
+ dependency: "direct dev"
+ description:
+ name: json_serializable
+ sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.8.0"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.0"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ lottie:
+ dependency: "direct main"
+ description:
+ name: lottie
+ sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.16+1"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.0"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.11.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
+ octo_image:
+ dependency: transitive
+ description:
+ name: octo_image
+ sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ path:
+ dependency: "direct main"
+ description:
+ name: path
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.0"
+ path_provider:
+ dependency: "direct main"
+ description:
+ name: path_provider
+ sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.3"
+ path_provider_android:
+ dependency: transitive
+ description:
+ name: path_provider_android
+ sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.4"
+ path_provider_foundation:
+ dependency: transitive
+ description:
+ name: path_provider_foundation
+ sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ path_provider_linux:
+ dependency: transitive
+ description:
+ name: path_provider_linux
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
+ path_provider_platform_interface:
+ dependency: transitive
+ description:
+ name: path_provider_platform_interface
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ path_provider_windows:
+ dependency: transitive
+ description:
+ name: path_provider_windows
+ sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.4"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.8"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.1"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.3"
+ riverpod:
+ dependency: transitive
+ description:
+ name: riverpod
+ sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.1"
+ riverpod_analyzer_utils:
+ dependency: transitive
+ description:
+ name: riverpod_analyzer_utils
+ sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.1"
+ riverpod_annotation:
+ dependency: "direct main"
+ description:
+ name: riverpod_annotation
+ sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.5"
+ riverpod_generator:
+ dependency: "direct dev"
+ description:
+ name: riverpod_generator
+ sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.0"
+ riverpod_lint:
+ dependency: "direct dev"
+ description:
+ name: riverpod_lint
+ sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.10"
+ rxdart:
+ dependency: transitive
+ description:
+ name: rxdart
+ sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.27.7"
+ scroll_loop_auto_scroll:
+ dependency: "direct main"
+ description:
+ name: scroll_loop_auto_scroll
+ sha256: "83645b380c58c9dac2a9948b11a6b09149a2aebd18a7ca25bbf3be3c89dbbbff"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.5"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ shelfsdk:
+ dependency: "direct main"
+ description:
+ path: "../../_dart/shelfsdk"
+ relative: true
+ source: path
+ version: "1.0.0"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ socket_io_client:
+ dependency: transitive
+ description:
+ name: socket_io_client
+ sha256: "15c9deae04e2b21065adbe864a641e0a5fc2499ff4c89e98a3877aea22ce841c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0-beta.2"
+ socket_io_common:
+ dependency: transitive
+ description:
+ name: socket_io_common
+ sha256: "5ad0f12e1b17f4300b0d6a27feaf1b7c1153ca21498ba307f0297b81449f7a44"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0-beta.0"
+ source_gen:
+ dependency: transitive
+ description:
+ name: source_gen
+ sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.0"
+ source_helper:
+ dependency: transitive
+ description:
+ name: source_helper
+ sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.4"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.0"
+ sprintf:
+ dependency: transitive
+ description:
+ name: sprintf
+ sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ sqflite:
+ dependency: transitive
+ description:
+ name: sqflite
+ sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.3+1"
+ sqflite_common:
+ dependency: transitive
+ description:
+ name: sqflite_common
+ sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.4"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.11.1"
+ state_notifier:
+ dependency: transitive
+ description:
+ name: state_notifier
+ sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ synchronized:
+ dependency: transitive
+ description:
+ name: synchronized
+ sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0+1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.1"
+ timing:
+ dependency: transitive
+ description:
+ name: timing
+ sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.2"
+ uuid:
+ dependency: transitive
+ description:
+ name: uuid
+ sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.4.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
+ url: "https://pub.dev"
+ source: hosted
+ version: "13.0.0"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.1"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.5"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.5.0"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+sdks:
+ dart: ">=3.3.4 <4.0.0"
+ flutter: ">=3.16.0"
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..e41bb60
--- /dev/null
+++ b/pubspec.yaml
@@ -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
diff --git a/web/favicon.png b/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
Binary files /dev/null and b/web/favicon.png differ
diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
Binary files /dev/null and b/web/icons/Icon-192.png differ
diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
Binary files /dev/null and b/web/icons/Icon-512.png differ
diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ
diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..911d70e
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ whispering_pages
+
+
+
+
+
+
+
+
+
+
diff --git a/web/manifest.json b/web/manifest.json
new file mode 100644
index 0000000..d3c5e16
--- /dev/null
+++ b/web/manifest.json
@@ -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"
+ }
+ ]
+}
diff --git a/windows/.gitignore b/windows/.gitignore
new file mode 100644
index 0000000..d492d0d
--- /dev/null
+++ b/windows/.gitignore
@@ -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/
diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt
new file mode 100644
index 0000000..6efacbb
--- /dev/null
+++ b/windows/CMakeLists.txt
@@ -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 "$<$:_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 "$")
+# 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)
diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000..903f489
--- /dev/null
+++ b/windows/flutter/CMakeLists.txt
@@ -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} $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..afc39a1
--- /dev/null
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,14 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+ IsarFlutterLibsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
+}
diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..dc139d8
--- /dev/null
+++ b/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2a57005
--- /dev/null
+++ b/windows/flutter/generated_plugins.cmake
@@ -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}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ 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}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000..394917c
--- /dev/null
+++ b/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, 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} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc
new file mode 100644
index 0000000..66d0a6a
--- /dev/null
+++ b/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.example" "\0"
+ VALUE "FileDescription", "whispering_pages" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "whispering_pages" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0"
+ VALUE "OriginalFilename", "whispering_pages.exe" "\0"
+ VALUE "ProductName", "whispering_pages" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000..955ee30
--- /dev/null
+++ b/windows/runner/flutter_window.cpp
@@ -0,0 +1,71 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h
new file mode 100644
index 0000000..6da0652
--- /dev/null
+++ b/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp
new file mode 100644
index 0000000..21ba1b7
--- /dev/null
+++ b/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.Create(L"whispering_pages", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/windows/runner/resource.h b/windows/runner/resource.h
new file mode 100644
index 0000000..66a65d1
--- /dev/null
+++ b/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico
new file mode 100644
index 0000000..c04e20c
Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ
diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000..a42ea76
--- /dev/null
+++ b/windows/runner/runner.exe.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp
new file mode 100644
index 0000000..b2b0873
--- /dev/null
+++ b/windows/runner/utils.cpp
@@ -0,0 +1,65 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ int input_length = (int)wcslen(utf16_string);
+ std::string utf8_string;
+ if (target_length <= 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/windows/runner/utils.h b/windows/runner/utils.h
new file mode 100644
index 0000000..3879d54
--- /dev/null
+++ b/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp
new file mode 100644
index 0000000..60608d0
--- /dev/null
+++ b/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include
+#include
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast