something

This commit is contained in:
Dr-Blank 2024-05-08 05:03:49 -04:00
parent dbf4ce1959
commit a720c977c2
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
115 changed files with 8819 additions and 1 deletions

43
.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: android
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: ios
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: linux
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: macos
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: web
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: windows
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

25
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "whispering_pages",
"request": "launch",
"type": "dart"
},
{
"name": "whispering_pages (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "whispering_pages (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View file

@ -3,5 +3,10 @@
"activityBar.background": "#5A1021", "activityBar.background": "#5A1021",
"titleBar.activeBackground": "#7E162E", "titleBar.activeBackground": "#7E162E",
"titleBar.activeForeground": "#FEFBFC" "titleBar.activeForeground": "#FEFBFC"
} },
"files.exclude": {
"**/*.freezed.dart": true,
"**/*.g.dart": true
},
"cSpell.words": ["Autovalidate", "mocktail", "riverpod", "shelfsdk"]
} }

37
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,37 @@
{
"version": "2.0.0",
"tasks": [
{
"icon": { "id": "eye-watch", "color": "terminal.ansiYellow" },
"label": "build_runner watch",
"type": "shell",
"command": "dart run build_runner watch",
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Running build_runner watch for code generation",
"presentation": {
"revealProblems": "onProblem",
"reveal": "silent",
"panel": "dedicated"
},
"runOptions": {
"instanceLimit": 1,
"runOn": "folderOpen",
"reevaluateOnRerun": true
},
"problemMatcher": {
"owner": "dart",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
}
}
]
}

16
README.md Normal file
View file

@ -0,0 +1,16 @@
# whispering_pages
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

33
analysis_options.yaml Normal file
View file

@ -0,0 +1,33 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
require_trailing_commas: true
analyzer:
errors:
invalid_annotation_target: ignore
plugins:
- custom_lint
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

67
android/app/build.gradle Normal file
View file

@ -0,0 +1,67 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.whispering_pages"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.whispering_pages"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,44 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="whispering_pages"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package com.example.whispering_pages
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

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

View file

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

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View file

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View file

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

26
android/settings.gradle Normal file
View file

@ -0,0 +1,26 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"

File diff suppressed because it is too large Load diff

143
lib/api/api_provider.dart Normal file
View file

@ -0,0 +1,143 @@
// provider to provide the api instance
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/db/cache_manager.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
part 'api_provider.g.dart';
Uri makeBaseUrl(String address) {
if (!address.startsWith('http') && !address.startsWith('https')) {
address = 'https://$address';
}
if (!Uri.parse(address).isAbsolute) {
throw ArgumentError.value(address, 'address', 'Invalid address');
}
return Uri.parse(address);
}
/// get the api instance for the given base url
@riverpod
AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
// try to get the base url from app settings
final apiSettings = ref.watch(apiSettingsProvider);
baseUrl ??= apiSettings.activeServer?.serverUrl;
if (baseUrl == null) {
throw ArgumentError.notNull('baseUrl');
}
return AudiobookshelfApi(
baseUrl: baseUrl,
);
}
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
@riverpod
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser;
if (user == null) {
throw StateError('No active user');
}
return AudiobookshelfApi(
baseUrl: Uri.https(user.server.serverUrl.toString()),
token: user.authToken,
);
}
/// ping the server to check if it is reachable
@riverpod
FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
// return (await ref.watch(audiobookshelfApiProvider).server.ping()) ?? false;
// if address not starts with http or https, add https
// !remove this line
// return true;
if (address.isEmpty) {
return false;
}
if (!address.startsWith('http') && !address.startsWith('https')) {
address = 'https://$address';
}
// check url is valid
if (!Uri.parse(address).isAbsolute) {
return false;
}
return await AudiobookshelfApi(baseUrl: Uri.parse(address)).server.ping() ??
false;
}
/// fetch the personalized view
@riverpod
class PersonalizedView extends _$PersonalizedView {
@override
Stream<List<Shelf>> build() async* {
final api = ref.watch(authenticatedApiProvider);
final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser;
if (apiSettings.activeLibraryId == null) {
// set it to default user library by logging in and getting the library id
final login =
await api.login(username: user!.username!, password: user.password!);
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId),
);
}
// try to find in cache
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}';
final cachedRes = await apiResponseCacheManager.getFileFromCache(
key,
);
if (cachedRes != null) {
final resJson = jsonDecode(await cachedRes.file.readAsString()) as List;
final res = [
for (final item in resJson)
Shelf.fromJson(item as Map<String, dynamic>),
];
debugPrint('reading from cache: $cachedRes');
yield res;
}
// ! exagerated delay
await Future.delayed(const Duration(seconds: 2));
final res = await api.libraries
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
// save to cache
final newFile = await apiResponseCacheManager.putFile(
key,
utf8.encode(jsonEncode(res)),
fileExtension: 'json',
key: key,
);
debugPrint('writing to cache: $newFile');
yield res!;
}
// method to force refresh the view and ignore the cache
Future<void> forceRefresh() async {
// clear the cache
return apiResponseCacheManager.emptyCache();
}
}
/// fetch continue listening audiobooks
@riverpod
FutureOr<GetUserSessionsResponse> fetchContinueListening(
FetchContinueListeningRef ref,
) async {
final api = ref.watch(authenticatedApiProvider);
final res = await api.me.getSessions();
// debugPrint(
// 'fetchContinueListening: ${res.sessions.map((e) => e.libraryItemId).toSet()}',
// );
return res!;
}

370
lib/api/api_provider.g.dart Normal file
View file

@ -0,0 +1,370 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$audiobookshelfApiHash() => r'5eb091c6b18c0bf5a0eec079fdb872a84c4f00d9';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
@ProviderFor(audiobookshelfApi)
const audiobookshelfApiProvider = AudiobookshelfApiFamily();
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
class AudiobookshelfApiFamily extends Family<AudiobookshelfApi> {
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
const AudiobookshelfApiFamily();
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
AudiobookshelfApiProvider call(
Uri? baseUrl,
) {
return AudiobookshelfApiProvider(
baseUrl,
);
}
@override
AudiobookshelfApiProvider getProviderOverride(
covariant AudiobookshelfApiProvider provider,
) {
return call(
provider.baseUrl,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'audiobookshelfApiProvider';
}
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
class AudiobookshelfApiProvider extends AutoDisposeProvider<AudiobookshelfApi> {
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
AudiobookshelfApiProvider(
Uri? baseUrl,
) : this._internal(
(ref) => audiobookshelfApi(
ref as AudiobookshelfApiRef,
baseUrl,
),
from: audiobookshelfApiProvider,
name: r'audiobookshelfApiProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$audiobookshelfApiHash,
dependencies: AudiobookshelfApiFamily._dependencies,
allTransitiveDependencies:
AudiobookshelfApiFamily._allTransitiveDependencies,
baseUrl: baseUrl,
);
AudiobookshelfApiProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.baseUrl,
}) : super.internal();
final Uri? baseUrl;
@override
Override overrideWith(
AudiobookshelfApi Function(AudiobookshelfApiRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AudiobookshelfApiProvider._internal(
(ref) => create(ref as AudiobookshelfApiRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
baseUrl: baseUrl,
),
);
}
@override
AutoDisposeProviderElement<AudiobookshelfApi> createElement() {
return _AudiobookshelfApiProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AudiobookshelfApiProvider && other.baseUrl == baseUrl;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, baseUrl.hashCode);
return _SystemHash.finish(hash);
}
}
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> {
/// The parameter `baseUrl` of this provider.
Uri? get baseUrl;
}
class _AudiobookshelfApiProviderElement
extends AutoDisposeProviderElement<AudiobookshelfApi>
with AudiobookshelfApiRef {
_AudiobookshelfApiProviderElement(super.provider);
@override
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
}
String _$authenticatedApiHash() => r'62213d5d0268eeaa2a16211cd60b1b6f0d19dd40';
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
///
/// Copied from [authenticatedApi].
@ProviderFor(authenticatedApi)
final authenticatedApiProvider =
AutoDisposeProvider<AudiobookshelfApi>.internal(
authenticatedApi,
name: r'authenticatedApiProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authenticatedApiHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AuthenticatedApiRef = AutoDisposeProviderRef<AudiobookshelfApi>;
String _$isServerAliveHash() => r'f839350795fbdeb0ca1d5f0c84a9065cac4dd40a';
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
@ProviderFor(isServerAlive)
const isServerAliveProvider = IsServerAliveFamily();
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
class IsServerAliveFamily extends Family<AsyncValue<bool>> {
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
const IsServerAliveFamily();
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
IsServerAliveProvider call(
String address,
) {
return IsServerAliveProvider(
address,
);
}
@override
IsServerAliveProvider getProviderOverride(
covariant IsServerAliveProvider provider,
) {
return call(
provider.address,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'isServerAliveProvider';
}
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
class IsServerAliveProvider extends AutoDisposeFutureProvider<bool> {
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
IsServerAliveProvider(
String address,
) : this._internal(
(ref) => isServerAlive(
ref as IsServerAliveRef,
address,
),
from: isServerAliveProvider,
name: r'isServerAliveProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$isServerAliveHash,
dependencies: IsServerAliveFamily._dependencies,
allTransitiveDependencies:
IsServerAliveFamily._allTransitiveDependencies,
address: address,
);
IsServerAliveProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.address,
}) : super.internal();
final String address;
@override
Override overrideWith(
FutureOr<bool> Function(IsServerAliveRef provider) create,
) {
return ProviderOverride(
origin: this,
override: IsServerAliveProvider._internal(
(ref) => create(ref as IsServerAliveRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
address: address,
),
);
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _IsServerAliveProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is IsServerAliveProvider && other.address == address;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, address.hashCode);
return _SystemHash.finish(hash);
}
}
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `address` of this provider.
String get address;
}
class _IsServerAliveProviderElement
extends AutoDisposeFutureProviderElement<bool> with IsServerAliveRef {
_IsServerAliveProviderElement(super.provider);
@override
String get address => (origin as IsServerAliveProvider).address;
}
String _$fetchContinueListeningHash() =>
r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d';
/// fetch continue listening audiobooks
///
/// Copied from [fetchContinueListening].
@ProviderFor(fetchContinueListening)
final fetchContinueListeningProvider =
AutoDisposeFutureProvider<GetUserSessionsResponse>.internal(
fetchContinueListening,
name: r'fetchContinueListeningProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchContinueListeningHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef FetchContinueListeningRef
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
String _$personalizedViewHash() => r'52a89c46ce668238ca11b5394fd1d14c910947f5';
/// fetch the personalized view
///
/// Copied from [PersonalizedView].
@ProviderFor(PersonalizedView)
final personalizedViewProvider =
AutoDisposeStreamNotifierProvider<PersonalizedView, List<Shelf>>.internal(
PersonalizedView.new,
name: r'personalizedViewProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$personalizedViewHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/api/server_provider.dart'
show audiobookShelfServerProvider;
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
import 'package:whispering_pages/settings/models/authenticated_user.dart' as model;
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/db/storage.dart';
part 'authenticated_user_provider.g.dart';
final _box = AvailableHiveBoxes.authenticatedUserBox;
/// provides with a set of authenticated users
@riverpod
class AuthenticatedUser extends _$AuthenticatedUser {
@override
Set<model.AuthenticatedUser> build() {
ref.listenSelf((_, __) {
writeStateToBox();
});
// get the app settings
final apiSettings = ref.read(apiSettingsProvider);
final availUsers = readFromBoxOrCreate();
if (apiSettings.activeUser != null) {
availUsers.add(apiSettings.activeUser!);
}
return availUsers;
}
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
if (_box.isNotEmpty) {
final foundData = _box.getRange(0, _box.length);
debugPrint('found users in box: $foundData');
return foundData.toSet();
} else {
debugPrint('no settings found in box');
return {};
}
}
void writeStateToBox() {
_box.clear();
if (state.isEmpty) {
return;
}
_box.addAll(state);
debugPrint('writing state to box: $state');
}
void addUser(model.AuthenticatedUser user) {
state = state..add(user);
}
void removeUsersOfServer(AudiobookShelfServer registeredServer) {
state = state.where((user) => user.server != registeredServer).toSet();
// remove the server from the server provider
final serverProvider = ref.read(audiobookShelfServerProvider);
if (serverProvider.contains(registeredServer)) {
ref
.read(audiobookShelfServerProvider.notifier)
.removeServer(registeredServer);
}
}
void removeUser(model.AuthenticatedUser user) {
state = state.where((u) => u != user).toSet();
// also remove the user from the active user
final apiSettings = ref.read(apiSettingsProvider);
if (apiSettings.activeUser == user) {
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeUser: null,
),
);
}
}
}

View file

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticated_user_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$authenticatedUserHash() => r'5702fb6ab1e83129d57c89ef02a65c5910f2a076';
/// provides with a set of authenticated users
///
/// Copied from [AuthenticatedUser].
@ProviderFor(AuthenticatedUser)
final authenticatedUserProvider = AutoDisposeNotifierProvider<AuthenticatedUser,
Set<model.AuthenticatedUser>>.internal(
AuthenticatedUser.new,
name: r'authenticatedUserProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authenticatedUserHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AuthenticatedUser = AutoDisposeNotifier<Set<model.AuthenticatedUser>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,55 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/db/cache_manager.dart';
/// provides cover images for the audiobooks
///
/// is a stream provider that provides cover images first from the cache then from the server
/// if the image is not found in the cache, it will be fetched from the server and saved to the cache
/// if the image is not found in the server it will throw an error
part 'image_provider.g.dart';
@riverpod
class CoverImage extends _$CoverImage {
@override
Stream<Uint8List> build(LibraryItem libraryItem) async* {
final api = ref.watch(authenticatedApiProvider);
// try to get the image from the cache
final file = await imageCacheManager.getFileFromCache(libraryItem.id);
if (file != null) {
// if the image is in the cache, yield it
yield await file.file.readAsBytes();
// return if no need to fetch from the server
if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
return;
} else {
debugPrint(
'cover image stale for ${libraryItem.id}, fetching from the server',
);
}
}
// check if the image is in the cache
final coverImage = await api.items.getCover(
libraryItemId: libraryItem.id,
parameters: const GetImageReqParams(width: 500),
);
// save the image to the cache
final newFile = await imageCacheManager.putFile(
libraryItem.id,
coverImage ?? Uint8List(0),
key: libraryItem.id,
);
debugPrint(
'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}',
);
yield coverImage ?? Uint8List(0);
}
}

View file

@ -0,0 +1,174 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'image_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$coverImageHash() => r'34c6aaf6831fea198984d22ecdf2c5b74e110891';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$CoverImage
extends BuildlessAutoDisposeStreamNotifier<Uint8List> {
late final LibraryItem libraryItem;
Stream<Uint8List> build(
LibraryItem libraryItem,
);
}
/// See also [CoverImage].
@ProviderFor(CoverImage)
const coverImageProvider = CoverImageFamily();
/// See also [CoverImage].
class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
/// See also [CoverImage].
const CoverImageFamily();
/// See also [CoverImage].
CoverImageProvider call(
LibraryItem libraryItem,
) {
return CoverImageProvider(
libraryItem,
);
}
@override
CoverImageProvider getProviderOverride(
covariant CoverImageProvider provider,
) {
return call(
provider.libraryItem,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'coverImageProvider';
}
/// See also [CoverImage].
class CoverImageProvider
extends AutoDisposeStreamNotifierProviderImpl<CoverImage, Uint8List> {
/// See also [CoverImage].
CoverImageProvider(
LibraryItem libraryItem,
) : this._internal(
() => CoverImage()..libraryItem = libraryItem,
from: coverImageProvider,
name: r'coverImageProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$coverImageHash,
dependencies: CoverImageFamily._dependencies,
allTransitiveDependencies:
CoverImageFamily._allTransitiveDependencies,
libraryItem: libraryItem,
);
CoverImageProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.libraryItem,
}) : super.internal();
final LibraryItem libraryItem;
@override
Stream<Uint8List> runNotifierBuild(
covariant CoverImage notifier,
) {
return notifier.build(
libraryItem,
);
}
@override
Override overrideWith(CoverImage Function() create) {
return ProviderOverride(
origin: this,
override: CoverImageProvider._internal(
() => create()..libraryItem = libraryItem,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
libraryItem: libraryItem,
),
);
}
@override
AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
createElement() {
return _CoverImageProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CoverImageProvider && other.libraryItem == libraryItem;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, libraryItem.hashCode);
return _SystemHash.finish(hash);
}
}
mixin CoverImageRef on AutoDisposeStreamNotifierProviderRef<Uint8List> {
/// The parameter `libraryItem` of this provider.
LibraryItem get libraryItem;
}
class _CoverImageProviderElement
extends AutoDisposeStreamNotifierProviderElement<CoverImage, Uint8List>
with CoverImageRef {
_CoverImageProviderElement(super.provider);
@override
LibraryItem get libraryItem => (origin as CoverImageProvider).libraryItem;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,102 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/db/storage.dart';
part 'server_provider.g.dart';
final _box = AvailableHiveBoxes.serverBox;
class ServerAlreadyExistsException implements Exception {
final model.AudiobookShelfServer server;
ServerAlreadyExistsException(this.server);
@override
String toString() {
return 'Server $server already exists';
}
}
/// provides with a set of servers added by the user
@riverpod
class AudiobookShelfServer extends _$AudiobookShelfServer {
@override
Set<model.AudiobookShelfServer> build() {
ref.listenSelf((_, __) {
writeStateToBox();
});
// get the app settings
final apiSettings = ref.read(apiSettingsProvider);
// is default server is present, add it to the set
final availableServers = readFromBoxOrCreate();
if (apiSettings.activeServer != null) {
availableServers.add(apiSettings.activeServer!);
}
// also add server of the user
if (apiSettings.activeUser != null) {
availableServers.add(apiSettings.activeUser!.server);
}
return availableServers;
}
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
if (_box.isNotEmpty) {
final foundServers = _box.getRange(0, _box.length);
debugPrint('found servers in box: $foundServers');
return foundServers.whereNotNull().toSet();
} else {
debugPrint('no settings found in box');
return {};
}
}
void writeStateToBox() {
_box.clear();
if (state.isEmpty) {
return;
}
_box.addAll(state);
debugPrint('writing state to box: $state');
}
void addServer(model.AudiobookShelfServer server) {
if (state.contains(server)) {
throw ServerAlreadyExistsException(server);
}
state = {...state, server};
}
void removeServer(model.AudiobookShelfServer server,
{
bool removeUsers = false,
}) {
state = state.where((s) => s != server).toSet();
// remove the server from the active server
final apiSettings = ref.read(apiSettingsProvider);
if (apiSettings.activeServer == server) {
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeServer: null,
),
);
}
// remove the users of this server
if (removeUsers) {
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server);
}
}
// ? this doesn't seem to be useful
void updateServer(model.AudiobookShelfServer newServer) {
state = state
.map(
(existingServer) =>
existingServer == newServer ? newServer : existingServer,
)
.toSet();
}
}

View file

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$audiobookShelfServerHash() =>
r'f0d645bb42233c59886bc43fdc473897484ceca1';
/// provides with a set of servers added by the user
///
/// Copied from [AudiobookShelfServer].
@ProviderFor(AudiobookShelfServer)
final audiobookShelfServerProvider = AutoDisposeNotifierProvider<
AudiobookShelfServer, Set<model.AudiobookShelfServer>>.internal(
AudiobookShelfServer.new,
name: r'audiobookShelfServerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$audiobookShelfServerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AudiobookShelfServer
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,22 @@
import 'package:flutter/foundation.dart' show immutable;
import 'package:hive/hive.dart';
import 'package:whispering_pages/settings/models/models.dart';
@immutable
class AvailableHiveBoxes {
const AvailableHiveBoxes._();
/// Box for storing user preferences as [AppSettings]
static final userPrefsBox = Hive.box<AppSettings>(name: 'userPrefs');
/// Box for storing [ApiSettings]
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
/// stores the a list of [AudiobookShelfServer]
static final serverBox =
Hive.box<AudiobookShelfServer>(name: 'audiobookShelfServer');
/// stores the a list of [AuthenticatedUser]
static final authenticatedUserBox =
Hive.box<AuthenticatedUser>(name: 'authenticatedUser');
}

1009
lib/db/cache/image.g.dart vendored Normal file

File diff suppressed because it is too large Load diff

39
lib/db/cache/schemas/image.dart vendored Normal file
View file

@ -0,0 +1,39 @@
import 'package:isar/isar.dart';
part '../image.g.dart';
/// Represents a cover image for a library item
///
/// stores 2 paths, one is thumbnail and the other is the full size image
/// both are optional
/// also stores last fetched date for the image
/// Id is passed as a parameter to the collection annotation (the lib_item_id)
/// also index the id
/// This is because the image is a part of the library item and the library item
/// is the parent of the image
@Collection(ignore: {'path'})
@Name('CacheImage')
class Image {
@Id()
int id;
String? thumbnailPath;
String? imagePath;
DateTime lastSaved;
Image({
required this.id,
this.thumbnailPath,
this.imagePath,
}) : lastSaved = DateTime.now();
/// returns the path to the image
String? get path => thumbnailPath ?? imagePath;
/// automatically updates the last fetched date when saving a new path
void updatePath(String? thumbnailPath, String? imagePath) async {
this.thumbnailPath = thumbnailPath;
this.imagePath = imagePath;
lastSaved = DateTime.now();
}
}

19
lib/db/cache_manager.dart Normal file
View file

@ -0,0 +1,19 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
final imageCacheManager = CacheManager(
Config(
'image_cache_manager',
stalePeriod: const Duration(days: 365 * 10),
repo: JsonCacheInfoRepository(),
maxNrOfCacheObjects: 1000,
),
);
final apiResponseCacheManager = CacheManager(
Config(
'api_response_cache_manager',
stalePeriod: const Duration(days: 1),
repo: JsonCacheInfoRepository(),
maxNrOfCacheObjects: 1000,
),
);

26
lib/db/init.dart Normal file
View file

@ -0,0 +1,26 @@
// does the initial setup of the storage
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:whispering_pages/settings/constants.dart';
import 'register_models.dart';
Future initStorage() async {
final dir = await getApplicationDocumentsDirectory();
// use whispering_pages as the directory for hive
final storageDir = Directory(p.join(
dir.path,
AppMetadata.appName.toLowerCase().replaceAll(' ', '_'),
),
);
await storageDir.create(recursive: true);
Hive.defaultDirectory = storageDir.path;
await registerModels();
}

View file

@ -0,0 +1,22 @@
import 'package:hive/hive.dart';
import 'package:whispering_pages/settings/models/models.dart';
// register all models to Hive for serialization
Future registerModels() async {
Hive.registerAdapter<AppSettings>(
'AppSettings',
((json) => AppSettings.fromJson(json)),
);
Hive.registerAdapter<ApiSettings>(
'ApiSettings',
((json) => ApiSettings.fromJson(json)),
);
Hive.registerAdapter<AudiobookShelfServer>(
'AudiobookShelfServer',
((json) => AudiobookShelfServer.fromJson(json)),
);
Hive.registerAdapter<AuthenticatedUser>(
'AuthenticatedUser',
((json) => AuthenticatedUser.fromJson(json)),
);
}

3
lib/db/storage.dart Normal file
View file

@ -0,0 +1,3 @@
export 'available_boxes.dart';
export 'init.dart';
export 'register_models.dart';

View file

@ -0,0 +1,79 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// A workaround for the issue where the autofill loses focus on Android
///
/// Example usage:
/// ```dart
/// InactiveFocusScopeObserver(
/// child: FormWithTheFeildsThatMayLooseFocus(),
/// )
/// ```
///
// see https://github.com/flutter/flutter/issues/137760#issuecomment-1956816977
class InactiveFocusScopeObserver extends StatefulWidget {
final Widget child;
const InactiveFocusScopeObserver({
super.key,
required this.child,
});
@override
State<InactiveFocusScopeObserver> createState() =>
_InactiveFocusScopeObserverState();
}
class _InactiveFocusScopeObserverState
extends State<InactiveFocusScopeObserver> {
final FocusScopeNode _focusScope = FocusScopeNode();
AppLifecycleListener? _listener;
FocusNode? _lastFocusedNode;
@override
void initState() {
_registerListener();
super.initState();
}
@override
Widget build(BuildContext context) => FocusScope(
node: _focusScope,
child: widget.child,
);
@override
void dispose() {
_listener?.dispose();
_focusScope.dispose();
super.dispose();
}
void _registerListener() {
/// optional if you want this workaround for any platform and not just for android
if (defaultTargetPlatform != TargetPlatform.android) {
return;
}
_listener = AppLifecycleListener(
onInactive: () {
_lastFocusedNode = _focusScope.focusedChild;
},
onResume: () {
_lastFocusedNode = null;
},
);
_focusScope.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_lastFocusedNode?.hasFocus == false) {
_lastFocusedNode?.requestFocus();
_lastFocusedNode = null;
}
}
}

44
lib/main.dart Normal file
View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/server_provider.dart';
import 'package:whispering_pages/db/storage.dart';
import 'package:whispering_pages/pages/onboarding/onboarding.dart';
import 'package:whispering_pages/pages/pages.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/theme/theme.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// initialize the storage
await initStorage();
runApp(const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final servers = ref.watch(audiobookShelfServerProvider);
final appSettings = ref.watch(appSettingsProvider);
final apiSettings = ref.watch(apiSettingsProvider);
bool needOnboarding() {
return apiSettings.activeUser == null || servers.isEmpty;
}
return MaterialApp(
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ref.watch(appSettingsProvider).isDarkMode
? ThemeMode.dark
: ThemeMode.light,
home: needOnboarding() ? const OnboardingPage() : const HomePage(),
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart';
import 'package:whispering_pages/api/server_provider.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
class AppSettingsPage extends HookConsumerWidget {
const AppSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final registeredServers = ref.watch(audiobookShelfServerProvider);
final registeredServersAsList = registeredServers.toList();
final availableUsers = ref.watch(authenticatedUserProvider);
final serverURIController = useTextEditingController();
final formKey = GlobalKey<FormState>();
return Scaffold(
appBar: AppBar(
title: const Text('App Settings'),
),
body: SettingsList(
sections: [
SettingsSection(
title: const Text('Appearance'),
tiles: [
SettingsTile.switchTile(
initialValue: appSettings.isDarkMode,
title: const Text('Dark Mode'),
leading: appSettings.isDarkMode
? const Icon(Icons.dark_mode)
: const Icon(Icons.light_mode),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).toggleDarkMode();
},
),
],
),
],
),
);
}
}

76
lib/pages/home_page.dart Normal file
View file

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import '../widgets/drawer.dart';
import '../widgets/shelves/home_shelf.dart';
class HomePage extends HookConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// hooks for the dark mode
final settings = ref.watch(appSettingsProvider);
final api = ref.watch(authenticatedApiProvider);
final views = ref.watch(personalizedViewProvider);
final scrollController = useScrollController();
return Scaffold(
appBar: AppBar(
title: GestureDetector(
child: const Text('Whispering Pages'),
onTap: () {
// scroll to the top of the page
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
// refresh the view
ref.invalidate(personalizedViewProvider);
},
),
),
drawer: const MyDrawer(),
body: Container(
child: views.when(
data: (data) {
final shelvesToDisplay = data
.where((element) => !element.id.contains('discover'))
.map(
(shelf) => HomeShelf(
title: Text(shelf.label),
shelf: shelf,
),
)
.toList();
return RefreshIndicator(
onRefresh: () async {
// await ref
// .read(personalizedViewProvider.notifier)
// .forceRefresh();
return ref.refresh(personalizedViewProvider);
},
child: ListView.separated(
itemBuilder: (context, index) => shelvesToDisplay[index],
separatorBuilder: (context, index) => Divider(
color: Theme.of(context).dividerColor.withOpacity(0.1),
indent: 16,
endIndent: 16,
),
itemCount: shelvesToDisplay.length,
controller: scrollController,
),
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stack) {
return Text('Error: $error');
},
),
),
);
}
}

View file

@ -0,0 +1,89 @@
import 'package:coast/coast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/pages/onboarding/server_setup.dart';
import 'package:whispering_pages/pages/onboarding/user_login.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
const _serverTag = 'server';
class OnboardingPage extends StatefulHookConsumerWidget {
const OnboardingPage({super.key});
@override
OnboardingPageState createState() => OnboardingPageState();
}
class OnboardingPageState extends ConsumerState<OnboardingPage> {
final coastController = CoastController();
@override
void dispose() {
coastController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final apiSettings = ref.watch(apiSettingsProvider);
final serverUriController = useTextEditingController(
text: apiSettings.activeServer?.serverUrl.toString(),
);
bool isUserLoginAvailable() {
return apiSettings.activeServer != null;
}
// ignore: invalid_use_of_protected_member
if (isUserLoginAvailable()) {
try {
coastController.animateTo(
beach: 1,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
} catch (e) {
debugPrint('Error: $e');
}
}
final beaches = [
Beach(
builder: (context) => FirstTimeServerSetupPage(
controller: serverUriController,
heroServerTag: _serverTag,
),
),
isUserLoginAvailable()
? Beach(
builder: (context) => FirstTimeUserLoginPage(
serverUriController: serverUriController,
heroServerTag: _serverTag,
),
)
: null,
].nonNulls.toList();
const activeStep = 0;
return Scaffold(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Coast(
beaches: beaches,
controller: coastController,
observers: [
CrabController(),
],
),
),
Positioned(
child: Container(),
),
],
),
);
}
}

View file

@ -0,0 +1,66 @@
import 'package:coast/coast.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/server_provider.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/widgets/add_new_server.dart';
class FirstTimeServerSetupPage extends HookConsumerWidget {
const FirstTimeServerSetupPage({
super.key,
required this.controller,
required this.heroServerTag,
});
final TextEditingController controller;
final String heroServerTag;
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Welcome to Whispering Pages'),
Crab(
tag: heroServerTag,
child: AddNewServer(
controller: controller,
allowEmpty: true,
onPressed: () {
var newServer = controller.text.isEmpty
? null
: model.AudiobookShelfServer(
serverUrl: Uri.parse(controller.text),
);
try {
// add the server to the list of servers
if (newServer != null) {
ref.read(audiobookShelfServerProvider.notifier).addServer(
newServer,
);
}
// else remove the server from the list of servers
else if (apiSettings.activeServer != null) {
ref
.read(audiobookShelfServerProvider.notifier)
.removeServer(apiSettings.activeServer!);
}
} on ServerAlreadyExistsException catch (e) {
newServer = e.server;
} finally {
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeServer: newServer,
),
);
}
},
),
),
],
),
);
}
}

View file

@ -0,0 +1,94 @@
import 'package:coast/coast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart'
show authenticatedUserProvider;
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
import 'package:whispering_pages/settings/models/authenticated_user.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/widgets/add_new_server.dart';
import 'package:whispering_pages/widgets/user_login.dart';
/// Once the user has selected a server, they can login to it.
class FirstTimeUserLoginPage extends HookConsumerWidget {
const FirstTimeUserLoginPage({
super.key,
required this.serverUriController,
required this.heroServerTag,
});
final TextEditingController serverUriController;
final String heroServerTag;
@override
Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController();
final passwordController = useTextEditingController();
final apiSettings = ref.watch(apiSettingsProvider);
final api = ref
.watch(audiobookshelfApiProvider(Uri.https(serverUriController.text)));
/// Login to the server and save the user
void loginAndSave() async {
final username = usernameController.text;
final password = passwordController.text;
final success = await api.login(username: username, password: password);
// debugPrint('Login success: $success');
if (success != null) {
var authenticatedUser = AuthenticatedUser(
server: AudiobookShelfServer(
serverUrl: Uri.parse(serverUriController.text),
),
id: success.user.id,
password: password,
username: username,
authToken: api.token!,
);
// add the user to the list of users
ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser);
// set the active user
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeUser: authenticatedUser,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login failed'),
),
);
// give focus back to the username field
}
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Crab(
tag: heroServerTag,
child: AddNewServer(
controller: serverUriController,
onPressed: () {},
readOnly: true,
),
),
const SizedBox(height: 30),
const Text('Login to server'),
const SizedBox(height: 30),
UserLogin(
usernameController: usernameController,
passwordController: passwordController,
onPressed: loginAndSave,
),
],
),
);
}
}

2
lib/pages/pages.dart Normal file
View file

@ -0,0 +1,2 @@
export 'home_page.dart';
export 'server_manager.dart';

View file

@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/authenticated_user_provider.dart';
import 'package:whispering_pages/api/server_provider.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart' as model;
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/widgets/add_new_server.dart';
class ServerManagerPage extends HookConsumerWidget {
const ServerManagerPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final registeredServers = ref.watch(audiobookShelfServerProvider);
final registeredServersAsList = registeredServers.toList();
final availableUsers = ref.watch(authenticatedUserProvider);
final serverURIController = useTextEditingController();
final formKey = GlobalKey<FormState>();
debugPrint('registered servers: $registeredServers');
debugPrint('available users: $availableUsers');
return Scaffold(
appBar: AppBar(
title: const Text('Setup Servers'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
// crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text(
'Registered Servers',
),
Expanded(
child: ListView.builder(
itemCount: registeredServers.length,
reverse: true,
itemBuilder: (context, index) {
var registeredServer = registeredServersAsList[index];
return ExpansionTile(
title: Text(registeredServer.serverUrl.toString()),
subtitle: Text(
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
),
trailing: IconButton(
icon: const Icon(Icons.delete),
// delete the server from the list of servers
onPressed: () {
ref
.read(audiobookShelfServerProvider.notifier)
.removeServer(registeredServer);
},
),
// children are list of users of this server
children: availableUsers
.where(
(element) => element.server == registeredServer,
)
.map(
(e) => ListTile(
title: Text(e.username ?? 'Anonymous'),
subtitle: Text(e.authToken),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
ref
.read(authenticatedUserProvider.notifier)
.removeUser(e);
},
),
),
)
.nonNulls
.toList(),
);
},
),
),
const SizedBox(height: 20),
Form(
key: formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: AddNewServer(
controller: serverURIController,
onPressed: () {
if (formKey.currentState!.validate()) {
try {
final newServer = model.AudiobookShelfServer(
serverUrl: Uri.parse(serverURIController.text),
);
ref
.read(audiobookShelfServerProvider.notifier)
.addServer(
newServer,
);
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeServer: newServer,
),
);
serverURIController.clear();
} on ServerAlreadyExistsException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid URL'),
),
);
}
},
),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,47 @@
// this provider is used to provide the Api settings to the app
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/settings/models/api_settings.dart' as model;
import 'package:whispering_pages/db/available_boxes.dart';
part 'api_settings_provider.g.dart';
final _box = AvailableHiveBoxes.apiSettingsBox;
@riverpod
class ApiSettings extends _$ApiSettings {
@override
model.ApiSettings build() {
state = readFromBoxOrCreate();
ref.listenSelf((_, __) {
writeToBox();
});
return state;
}
model.ApiSettings readFromBoxOrCreate() {
// see if the settings are already in the box
if (_box.isNotEmpty) {
final foundSettings = _box.getAt(0);
debugPrint('found api settings in box: $foundSettings');
return foundSettings;
} else {
// create a new settings object
const settings = model.ApiSettings();
debugPrint('created new api settings: $settings');
return settings;
}
}
// write the settings to the box
void writeToBox() {
_box.clear();
_box.add(state);
debugPrint('wrote api settings to box: $state');
}
void updateState(model.ApiSettings newSettings) {
state = newSettings;
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_settings_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$apiSettingsHash() => r'a6927751bd91ec7c9e1a2810dc939407d9112210';
/// See also [ApiSettings].
@ProviderFor(ApiSettings)
final apiSettingsProvider =
AutoDisposeNotifierProvider<ApiSettings, model.ApiSettings>.internal(
ApiSettings.new,
name: r'apiSettingsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ApiSettings = AutoDisposeNotifier<model.ApiSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,51 @@
// this provider is used to provide the app settings to the app
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
import 'package:whispering_pages/db/available_boxes.dart';
part 'app_settings_provider.g.dart';
final _box = AvailableHiveBoxes.userPrefsBox;
@riverpod
class AppSettings extends _$AppSettings {
@override
model.AppSettings build() {
state = readFromBoxOrCreate();
ref.listenSelf((_, __) {
writeToBox();
});
return state;
}
model.AppSettings readFromBoxOrCreate() {
// see if the settings are already in the box
if (_box.isNotEmpty) {
final foundSettings = _box.getAt(0);
debugPrint('found settings in box: $foundSettings');
return foundSettings;
} else {
// create a new settings object
const settings = model.AppSettings();
debugPrint('created new settings: $settings');
return settings;
}
}
// write the settings to the box
void writeToBox() {
_box.clear();
_box.add(state);
debugPrint('wrote settings to box: $state');
}
void toggleDarkMode() {
state = state.copyWith(isDarkMode: !state.isDarkMode);
}
void updateState(model.AppSettings newSettings) {
state = newSettings;
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appSettingsHash() => r'da2cd1bb0da6e136e906bc61f29da89d0c5f53fb';
/// See also [AppSettings].
@ProviderFor(AppSettings)
final appSettingsProvider =
AutoDisposeNotifierProvider<AppSettings, model.AppSettings>.internal(
AppSettings.new,
name: r'appSettingsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppSettings = AutoDisposeNotifier<model.AppSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,8 @@
import 'package:flutter/foundation.dart' show immutable;
@immutable
class AppMetadata {
const AppMetadata._();
static const String appName = 'Whispering Pages';
}

View file

@ -0,0 +1,23 @@
// a freezed class to store the settings of the app
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
import 'package:whispering_pages/settings/models/authenticated_user.dart';
part 'api_settings.freezed.dart';
part 'api_settings.g.dart';
/// stores the settings for the active server and user
///
/// all settings that are needed to interact with the server are stored here
@freezed
class ApiSettings with _$ApiSettings {
const factory ApiSettings({
AudiobookShelfServer? activeServer,
AuthenticatedUser? activeUser,
String? activeLibraryId,
}) = _ApiSettings;
factory ApiSettings.fromJson(Map<String, dynamic> json) =>
_$ApiSettingsFromJson(json);
}

View file

@ -0,0 +1,229 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'api_settings.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
ApiSettings _$ApiSettingsFromJson(Map<String, dynamic> json) {
return _ApiSettings.fromJson(json);
}
/// @nodoc
mixin _$ApiSettings {
AudiobookShelfServer? get activeServer => throw _privateConstructorUsedError;
AuthenticatedUser? get activeUser => throw _privateConstructorUsedError;
String? get activeLibraryId => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ApiSettingsCopyWith<ApiSettings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ApiSettingsCopyWith<$Res> {
factory $ApiSettingsCopyWith(
ApiSettings value, $Res Function(ApiSettings) then) =
_$ApiSettingsCopyWithImpl<$Res, ApiSettings>;
@useResult
$Res call(
{AudiobookShelfServer? activeServer,
AuthenticatedUser? activeUser,
String? activeLibraryId});
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
$AuthenticatedUserCopyWith<$Res>? get activeUser;
}
/// @nodoc
class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
implements $ApiSettingsCopyWith<$Res> {
_$ApiSettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeServer = freezed,
Object? activeUser = freezed,
Object? activeLibraryId = freezed,
}) {
return _then(_value.copyWith(
activeServer: freezed == activeServer
? _value.activeServer
: activeServer // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer?,
activeUser: freezed == activeUser
? _value.activeUser
: activeUser // ignore: cast_nullable_to_non_nullable
as AuthenticatedUser?,
activeLibraryId: freezed == activeLibraryId
? _value.activeLibraryId
: activeLibraryId // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AudiobookShelfServerCopyWith<$Res>? get activeServer {
if (_value.activeServer == null) {
return null;
}
return $AudiobookShelfServerCopyWith<$Res>(_value.activeServer!, (value) {
return _then(_value.copyWith(activeServer: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$AuthenticatedUserCopyWith<$Res>? get activeUser {
if (_value.activeUser == null) {
return null;
}
return $AuthenticatedUserCopyWith<$Res>(_value.activeUser!, (value) {
return _then(_value.copyWith(activeUser: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ApiSettingsImplCopyWith<$Res>
implements $ApiSettingsCopyWith<$Res> {
factory _$$ApiSettingsImplCopyWith(
_$ApiSettingsImpl value, $Res Function(_$ApiSettingsImpl) then) =
__$$ApiSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AudiobookShelfServer? activeServer,
AuthenticatedUser? activeUser,
String? activeLibraryId});
@override
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
@override
$AuthenticatedUserCopyWith<$Res>? get activeUser;
}
/// @nodoc
class __$$ApiSettingsImplCopyWithImpl<$Res>
extends _$ApiSettingsCopyWithImpl<$Res, _$ApiSettingsImpl>
implements _$$ApiSettingsImplCopyWith<$Res> {
__$$ApiSettingsImplCopyWithImpl(
_$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeServer = freezed,
Object? activeUser = freezed,
Object? activeLibraryId = freezed,
}) {
return _then(_$ApiSettingsImpl(
activeServer: freezed == activeServer
? _value.activeServer
: activeServer // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer?,
activeUser: freezed == activeUser
? _value.activeUser
: activeUser // ignore: cast_nullable_to_non_nullable
as AuthenticatedUser?,
activeLibraryId: freezed == activeLibraryId
? _value.activeLibraryId
: activeLibraryId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ApiSettingsImpl implements _ApiSettings {
const _$ApiSettingsImpl(
{this.activeServer, this.activeUser, this.activeLibraryId});
factory _$ApiSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$ApiSettingsImplFromJson(json);
@override
final AudiobookShelfServer? activeServer;
@override
final AuthenticatedUser? activeUser;
@override
final String? activeLibraryId;
@override
String toString() {
return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ApiSettingsImpl &&
(identical(other.activeServer, activeServer) ||
other.activeServer == activeServer) &&
(identical(other.activeUser, activeUser) ||
other.activeUser == activeUser) &&
(identical(other.activeLibraryId, activeLibraryId) ||
other.activeLibraryId == activeLibraryId));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, activeServer, activeUser, activeLibraryId);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
__$$ApiSettingsImplCopyWithImpl<_$ApiSettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ApiSettingsImplToJson(
this,
);
}
}
abstract class _ApiSettings implements ApiSettings {
const factory _ApiSettings(
{final AudiobookShelfServer? activeServer,
final AuthenticatedUser? activeUser,
final String? activeLibraryId}) = _$ApiSettingsImpl;
factory _ApiSettings.fromJson(Map<String, dynamic> json) =
_$ApiSettingsImpl.fromJson;
@override
AudiobookShelfServer? get activeServer;
@override
AuthenticatedUser? get activeUser;
@override
String? get activeLibraryId;
@override
@JsonKey(ignore: true)
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_settings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map<String, dynamic> json) =>
_$ApiSettingsImpl(
activeServer: json['activeServer'] == null
? null
: AudiobookShelfServer.fromJson(
json['activeServer'] as Map<String, dynamic>),
activeUser: json['activeUser'] == null
? null
: AuthenticatedUser.fromJson(
json['activeUser'] as Map<String, dynamic>),
activeLibraryId: json['activeLibraryId'] as String?,
);
Map<String, dynamic> _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) =>
<String, dynamic>{
'activeServer': instance.activeServer,
'activeUser': instance.activeUser,
'activeLibraryId': instance.activeLibraryId,
};

View file

@ -0,0 +1,19 @@
// a freezed class to store the settings of the app
import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_settings.freezed.dart';
part 'app_settings.g.dart';
/// stores the settings of the app
///
/// only the visual settings are stored here
@freezed
class AppSettings with _$AppSettings {
const factory AppSettings({
@Default(true) bool isDarkMode,
}) = _AppSettings;
factory AppSettings.fromJson(Map<String, dynamic> json) =>
_$AppSettingsFromJson(json);
}

View file

@ -0,0 +1,153 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'app_settings.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) {
return _AppSettings.fromJson(json);
}
/// @nodoc
mixin _$AppSettings {
bool get isDarkMode => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AppSettingsCopyWith<AppSettings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(
AppSettings value, $Res Function(AppSettings) then) =
_$AppSettingsCopyWithImpl<$Res, AppSettings>;
@useResult
$Res call({bool isDarkMode});
}
/// @nodoc
class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
implements $AppSettingsCopyWith<$Res> {
_$AppSettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isDarkMode = null,
}) {
return _then(_value.copyWith(
isDarkMode: null == isDarkMode
? _value.isDarkMode
: isDarkMode // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$AppSettingsImplCopyWith<$Res>
implements $AppSettingsCopyWith<$Res> {
factory _$$AppSettingsImplCopyWith(
_$AppSettingsImpl value, $Res Function(_$AppSettingsImpl) then) =
__$$AppSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isDarkMode});
}
/// @nodoc
class __$$AppSettingsImplCopyWithImpl<$Res>
extends _$AppSettingsCopyWithImpl<$Res, _$AppSettingsImpl>
implements _$$AppSettingsImplCopyWith<$Res> {
__$$AppSettingsImplCopyWithImpl(
_$AppSettingsImpl _value, $Res Function(_$AppSettingsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isDarkMode = null,
}) {
return _then(_$AppSettingsImpl(
isDarkMode: null == isDarkMode
? _value.isDarkMode
: isDarkMode // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AppSettingsImpl implements _AppSettings {
const _$AppSettingsImpl({this.isDarkMode = true});
factory _$AppSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$AppSettingsImplFromJson(json);
@override
@JsonKey()
final bool isDarkMode;
@override
String toString() {
return 'AppSettings(isDarkMode: $isDarkMode)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AppSettingsImpl &&
(identical(other.isDarkMode, isDarkMode) ||
other.isDarkMode == isDarkMode));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, isDarkMode);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
__$$AppSettingsImplCopyWithImpl<_$AppSettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AppSettingsImplToJson(
this,
);
}
}
abstract class _AppSettings implements AppSettings {
const factory _AppSettings({final bool isDarkMode}) = _$AppSettingsImpl;
factory _AppSettings.fromJson(Map<String, dynamic> json) =
_$AppSettingsImpl.fromJson;
@override
bool get isDarkMode;
@override
@JsonKey(ignore: true)
_$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,17 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AppSettingsImpl _$$AppSettingsImplFromJson(Map<String, dynamic> json) =>
_$AppSettingsImpl(
isDarkMode: json['isDarkMode'] as bool? ?? true,
);
Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
<String, dynamic>{
'isDarkMode': instance.isDarkMode,
};

View file

@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'audiobookshelf_server.freezed.dart';
part 'audiobookshelf_server.g.dart';
typedef AudiobookShelfUri = Uri;
/// Represents a audiobookshelf server
@freezed
class AudiobookShelfServer with _$AudiobookShelfServer {
const factory AudiobookShelfServer({
required AudiobookShelfUri serverUrl,
// String? serverName,
}) = _AudiobookShelfServer;
factory AudiobookShelfServer.fromJson(Map<String, dynamic> json) =>
_$AudiobookShelfServerFromJson(json);
}

View file

@ -0,0 +1,156 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'audiobookshelf_server.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AudiobookShelfServer _$AudiobookShelfServerFromJson(Map<String, dynamic> json) {
return _AudiobookShelfServer.fromJson(json);
}
/// @nodoc
mixin _$AudiobookShelfServer {
Uri get serverUrl => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AudiobookShelfServerCopyWith<AudiobookShelfServer> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AudiobookShelfServerCopyWith<$Res> {
factory $AudiobookShelfServerCopyWith(AudiobookShelfServer value,
$Res Function(AudiobookShelfServer) then) =
_$AudiobookShelfServerCopyWithImpl<$Res, AudiobookShelfServer>;
@useResult
$Res call({Uri serverUrl});
}
/// @nodoc
class _$AudiobookShelfServerCopyWithImpl<$Res,
$Val extends AudiobookShelfServer>
implements $AudiobookShelfServerCopyWith<$Res> {
_$AudiobookShelfServerCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? serverUrl = null,
}) {
return _then(_value.copyWith(
serverUrl: null == serverUrl
? _value.serverUrl
: serverUrl // ignore: cast_nullable_to_non_nullable
as Uri,
) as $Val);
}
}
/// @nodoc
abstract class _$$AudiobookShelfServerImplCopyWith<$Res>
implements $AudiobookShelfServerCopyWith<$Res> {
factory _$$AudiobookShelfServerImplCopyWith(_$AudiobookShelfServerImpl value,
$Res Function(_$AudiobookShelfServerImpl) then) =
__$$AudiobookShelfServerImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({Uri serverUrl});
}
/// @nodoc
class __$$AudiobookShelfServerImplCopyWithImpl<$Res>
extends _$AudiobookShelfServerCopyWithImpl<$Res, _$AudiobookShelfServerImpl>
implements _$$AudiobookShelfServerImplCopyWith<$Res> {
__$$AudiobookShelfServerImplCopyWithImpl(_$AudiobookShelfServerImpl _value,
$Res Function(_$AudiobookShelfServerImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? serverUrl = null,
}) {
return _then(_$AudiobookShelfServerImpl(
serverUrl: null == serverUrl
? _value.serverUrl
: serverUrl // ignore: cast_nullable_to_non_nullable
as Uri,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AudiobookShelfServerImpl implements _AudiobookShelfServer {
const _$AudiobookShelfServerImpl({required this.serverUrl});
factory _$AudiobookShelfServerImpl.fromJson(Map<String, dynamic> json) =>
_$$AudiobookShelfServerImplFromJson(json);
@override
final Uri serverUrl;
@override
String toString() {
return 'AudiobookShelfServer(serverUrl: $serverUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AudiobookShelfServerImpl &&
(identical(other.serverUrl, serverUrl) ||
other.serverUrl == serverUrl));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, serverUrl);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
get copyWith =>
__$$AudiobookShelfServerImplCopyWithImpl<_$AudiobookShelfServerImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AudiobookShelfServerImplToJson(
this,
);
}
}
abstract class _AudiobookShelfServer implements AudiobookShelfServer {
const factory _AudiobookShelfServer({required final Uri serverUrl}) =
_$AudiobookShelfServerImpl;
factory _AudiobookShelfServer.fromJson(Map<String, dynamic> json) =
_$AudiobookShelfServerImpl.fromJson;
@override
Uri get serverUrl;
@override
@JsonKey(ignore: true)
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
get copyWith => throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audiobookshelf_server.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson(
Map<String, dynamic> json) =>
_$AudiobookShelfServerImpl(
serverUrl: Uri.parse(json['serverUrl'] as String),
);
Map<String, dynamic> _$$AudiobookShelfServerImplToJson(
_$AudiobookShelfServerImpl instance) =>
<String, dynamic>{
'serverUrl': instance.serverUrl.toString(),
};

View file

@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
part 'authenticated_user.freezed.dart';
part 'authenticated_user.g.dart';
/// authenticated user with server and credentials
@freezed
class AuthenticatedUser with _$AuthenticatedUser {
const factory AuthenticatedUser({
required AudiobookShelfServer server,
required String authToken,
String? id,
String? username,
String? password,
}) = _AuthenticatedUser;
factory AuthenticatedUser.fromJson(Map<String, dynamic> json) =>
_$AuthenticatedUserFromJson(json);
}

View file

@ -0,0 +1,253 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'authenticated_user.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AuthenticatedUser _$AuthenticatedUserFromJson(Map<String, dynamic> json) {
return _AuthenticatedUser.fromJson(json);
}
/// @nodoc
mixin _$AuthenticatedUser {
AudiobookShelfServer get server => throw _privateConstructorUsedError;
String get authToken => throw _privateConstructorUsedError;
String? get id => throw _privateConstructorUsedError;
String? get username => throw _privateConstructorUsedError;
String? get password => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AuthenticatedUserCopyWith<AuthenticatedUser> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AuthenticatedUserCopyWith<$Res> {
factory $AuthenticatedUserCopyWith(
AuthenticatedUser value, $Res Function(AuthenticatedUser) then) =
_$AuthenticatedUserCopyWithImpl<$Res, AuthenticatedUser>;
@useResult
$Res call(
{AudiobookShelfServer server,
String authToken,
String? id,
String? username,
String? password});
$AudiobookShelfServerCopyWith<$Res> get server;
}
/// @nodoc
class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
implements $AuthenticatedUserCopyWith<$Res> {
_$AuthenticatedUserCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? server = null,
Object? authToken = null,
Object? id = freezed,
Object? username = freezed,
Object? password = freezed,
}) {
return _then(_value.copyWith(
server: null == server
? _value.server
: server // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer,
authToken: null == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String,
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
username: freezed == username
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String?,
password: freezed == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AudiobookShelfServerCopyWith<$Res> get server {
return $AudiobookShelfServerCopyWith<$Res>(_value.server, (value) {
return _then(_value.copyWith(server: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$AuthenticatedUserImplCopyWith<$Res>
implements $AuthenticatedUserCopyWith<$Res> {
factory _$$AuthenticatedUserImplCopyWith(_$AuthenticatedUserImpl value,
$Res Function(_$AuthenticatedUserImpl) then) =
__$$AuthenticatedUserImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AudiobookShelfServer server,
String authToken,
String? id,
String? username,
String? password});
@override
$AudiobookShelfServerCopyWith<$Res> get server;
}
/// @nodoc
class __$$AuthenticatedUserImplCopyWithImpl<$Res>
extends _$AuthenticatedUserCopyWithImpl<$Res, _$AuthenticatedUserImpl>
implements _$$AuthenticatedUserImplCopyWith<$Res> {
__$$AuthenticatedUserImplCopyWithImpl(_$AuthenticatedUserImpl _value,
$Res Function(_$AuthenticatedUserImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? server = null,
Object? authToken = null,
Object? id = freezed,
Object? username = freezed,
Object? password = freezed,
}) {
return _then(_$AuthenticatedUserImpl(
server: null == server
? _value.server
: server // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer,
authToken: null == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String,
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
username: freezed == username
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String?,
password: freezed == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AuthenticatedUserImpl implements _AuthenticatedUser {
const _$AuthenticatedUserImpl(
{required this.server,
required this.authToken,
this.id,
this.username,
this.password});
factory _$AuthenticatedUserImpl.fromJson(Map<String, dynamic> json) =>
_$$AuthenticatedUserImplFromJson(json);
@override
final AudiobookShelfServer server;
@override
final String authToken;
@override
final String? id;
@override
final String? username;
@override
final String? password;
@override
String toString() {
return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username, password: $password)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthenticatedUserImpl &&
(identical(other.server, server) || other.server == server) &&
(identical(other.authToken, authToken) ||
other.authToken == authToken) &&
(identical(other.id, id) || other.id == id) &&
(identical(other.username, username) ||
other.username == username) &&
(identical(other.password, password) ||
other.password == password));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, server, authToken, id, username, password);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
__$$AuthenticatedUserImplCopyWithImpl<_$AuthenticatedUserImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AuthenticatedUserImplToJson(
this,
);
}
}
abstract class _AuthenticatedUser implements AuthenticatedUser {
const factory _AuthenticatedUser(
{required final AudiobookShelfServer server,
required final String authToken,
final String? id,
final String? username,
final String? password}) = _$AuthenticatedUserImpl;
factory _AuthenticatedUser.fromJson(Map<String, dynamic> json) =
_$AuthenticatedUserImpl.fromJson;
@override
AudiobookShelfServer get server;
@override
String get authToken;
@override
String? get id;
@override
String? get username;
@override
String? get password;
@override
@JsonKey(ignore: true)
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticated_user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
Map<String, dynamic> json) =>
_$AuthenticatedUserImpl(
server:
AudiobookShelfServer.fromJson(json['server'] as Map<String, dynamic>),
authToken: json['authToken'] as String,
id: json['id'] as String?,
username: json['username'] as String?,
password: json['password'] as String?,
);
Map<String, dynamic> _$$AuthenticatedUserImplToJson(
_$AuthenticatedUserImpl instance) =>
<String, dynamic>{
'server': instance.server,
'authToken': instance.authToken,
'id': instance.id,
'username': instance.username,
'password': instance.password,
};

View file

@ -0,0 +1,4 @@
export 'api_settings.dart';
export 'app_settings.dart';
export 'audiobookshelf_server.dart';
export 'authenticated_user.dart';

View file

@ -0,0 +1,2 @@
export 'app_settings_provider.dart';
export 'constants.dart';

8
lib/theme/dark.dart Normal file
View file

@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
colorScheme: ColorScheme.dark(
background: Colors.grey[900]!,
),
);

8
lib/theme/light.dart Normal file
View file

@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
final ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
colorScheme: ColorScheme.light(
background: Colors.grey[200]!,
),
);

2
lib/theme/theme.dart Normal file
View file

@ -0,0 +1,2 @@
export 'dark.dart';
export 'light.dart';

View file

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/api/api_provider.dart';
class AddNewServer extends HookConsumerWidget {
const AddNewServer({
super.key,
this.controller,
this.onPressed,
this.readOnly = false,
this.allowEmpty = false,
});
final TextEditingController? controller;
/// the function to call when the button is pressed
final void Function()? onPressed;
/// if this field is read only
final bool readOnly;
/// the server URI can be empty
final bool allowEmpty;
@override
Widget build(BuildContext context, WidgetRef ref) {
final myController = controller ?? useTextEditingController();
var newServerURI = useValueListenable(myController);
final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text));
bool isServerAliveValue = isServerAlive.when(
data: (value) => value,
loading: () => false,
error: (error, _) => false,
);
return TextFormField(
readOnly: readOnly,
controller: controller,
keyboardType: TextInputType.url,
autofillHints: const [AutofillHints.url],
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: 'Server URI',
labelStyle: TextStyle(
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
),
border: const OutlineInputBorder(),
prefixText: 'https://',
prefixIcon: Tooltip(
message: newServerURI.text.isEmpty
? 'Server Status'
: isServerAliveValue
? 'Server connected'
: 'Cannot connect to server',
child: newServerURI.text.isEmpty
? Icon(
Icons.cloud_outlined,
color: Theme.of(context).colorScheme.onBackground,
)
: isServerAlive.when(
data: (value) {
return value
? Icon(
Icons.cloud_done_outlined,
color: Theme.of(context).colorScheme.primary,
)
: Icon(
Icons.cloud_off_outlined,
color: Theme.of(context).colorScheme.error,
);
},
loading: () => Transform.scale(
scale: 0.5,
child: const CircularProgressIndicator(),
),
error: (error, _) => Icon(
Icons.cloud_off_outlined,
color: Theme.of(context).colorScheme.error,
),
),
),
// add server button
suffixIcon: onPressed == null
? null
: Container(
margin: const EdgeInsets.only(left: 8, right: 8),
child: IconButton.filled(
icon: const Icon(Icons.add),
tooltip: 'Add new server',
color: Theme.of(context).colorScheme.inversePrimary,
focusColor: Theme.of(context).colorScheme.onBackground,
// should be enabled when
onPressed: !readOnly &&
(isServerAliveValue ||
(allowEmpty && newServerURI.text.isEmpty))
? onPressed
: null, // disable button if server is not alive
),
),
),
);
// add to add to existing servers
}
}

49
lib/widgets/drawer.dart Normal file
View file

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:whispering_pages/pages/app_settings.dart';
import 'package:whispering_pages/pages/server_manager.dart';
class MyDrawer extends StatelessWidget {
const MyDrawer({
super.key,
});
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: [
const DrawerHeader(
child: Text(
'Whispering Pages',
style: TextStyle(
fontStyle: FontStyle.italic,
fontSize: 30,
),
),
),
ListTile(
title: const Text('server Settings'),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ServerManagerPage(),
),
);
},
),
ListTile(
title: const Text('App Settings'),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AppSettingsPage(),
),
);
},
),
],
),
);
}
}

View file

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/api/image_provider.dart';
import 'package:whispering_pages/widgets/shelves/home_shelf.dart';
/// A shelf that displays Authors on the home page
class AuthorHomeShelf extends HookConsumerWidget {
const AuthorHomeShelf({
super.key,
required this.shelf,
required this.title,
});
final Widget title;
final AuthorShelf shelf;
@override
Widget build(BuildContext context, WidgetRef ref) {
return SimpleHomeShelf(
title: title,
children: shelf.entities
.map(
(item) => AuthorOnShelf(item: item),
)
.toList(),
);
}
}
// a widget to display a item on the shelf
class AuthorOnShelf extends HookConsumerWidget {
const AuthorOnShelf({
super.key,
required this.item,
});
final Author item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final author = AuthorMinified.fromJson(item.toJson());
// final coverImage = ref.watch(coverImageProvider(item));
return Container(
margin: const EdgeInsets.only(right: 10, bottom: 10),
constraints: const BoxConstraints(maxWidth: 100),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: AspectRatio(
aspectRatio: 1,
child: Container(
constraints: const BoxConstraints(maxWidth: 50),
// child: coverImage.when(
// data: (image) {
// return Image.memory(image, fit: BoxFit.cover);
// },
// loading: () {
// return const Center(child: CircularProgressIndicator());
// },
// error: (error, stack) {
// return const Icon(Icons.error);
// },
// ),
),
),
),
Container(
margin: const EdgeInsets.all(5),
child: Text(
author.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}

View file

@ -0,0 +1,118 @@
import 'package:auto_scroll_text/auto_scroll_text.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/api/image_provider.dart';
import 'package:whispering_pages/widgets/shelves/home_shelf.dart';
/// A shelf that displays books on the home page
class BookHomeShelf extends HookConsumerWidget {
const BookHomeShelf({
super.key,
required this.shelf,
required this.title,
});
final Widget title;
final LibraryItemShelf shelf;
@override
Widget build(BuildContext context, WidgetRef ref) {
return SimpleHomeShelf(
title: title,
children: shelf.entities
.map(
(item) => switch (item.mediaType) {
MediaType.book => BookOnShelf(
item: item,
key: ValueKey(shelf.id + item.id),
),
_ => Container(),
},
)
.toList(),
);
}
}
// a widget to display a item on the shelf
class BookOnShelf extends HookConsumerWidget {
const BookOnShelf({
super.key,
required this.item,
});
final LibraryItem item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = BookMinified.fromJson(item.media.toJson());
final metadata = BookMetadataMinified.fromJson(book.metadata.toJson());
final coverImage = ref.watch(coverImageProvider(item));
const coverSize = 150.0;
return Container(
margin: const EdgeInsets.only(right: 10, bottom: 10),
constraints: const BoxConstraints(maxWidth: coverSize),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: AspectRatio(
aspectRatio: 1,
child: Container(
constraints: const BoxConstraints(maxWidth: coverSize),
color: Colors.grey[800],
child: coverImage.when(
data: (image) {
if (image.isEmpty) {
return const Icon(Icons.error);
}
return Image.memory(
image,
fit: BoxFit.cover,
cacheWidth:
(coverSize * MediaQuery.of(context).devicePixelRatio)
.round(),
);
},
loading: () {
return const Center(child: CircularProgressIndicator());
},
error: (error, stack) {
return const Icon(Icons.error);
},
),
),
),
),
Container(
margin: const EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AutoScrollText(
metadata.title ?? '',
mode: AutoScrollTextMode.bouncing,
curve: Curves.easeInOut,
velocity: const Velocity(pixelsPerSecond: Offset(15, 0)),
delayBefore: const Duration(seconds: 2),
pauseBetween: const Duration(seconds: 2),
numberOfReps: 15,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
Text(
metadata.authorName ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/widgets/shelves/author_shelf.dart';
import 'package:whispering_pages/widgets/shelves/book_shelf.dart';
/// A shelf that displays books/authors/series on the home page
///
/// this will build the appropriate shelf based on the type of the shelf
class HomeShelf extends HookConsumerWidget {
const HomeShelf({
super.key,
required this.shelf,
required this.title,
});
final Widget title;
final Shelf shelf;
@override
Widget build(BuildContext context, WidgetRef ref) {
return switch (shelf.type) {
ShelfType.book => BookHomeShelf(
title: title,
shelf: LibraryItemShelf.fromJson(shelf.toJson()),
),
ShelfType.authors => AuthorHomeShelf(
title: title,
shelf: AuthorShelf.fromJson(shelf.toJson()),
),
_ => Container(),
};
}
}
/// A shelf that displays books on the home page
class SimpleHomeShelf extends HookConsumerWidget {
const SimpleHomeShelf({
super.key,
required this.children,
required this.title,
});
final Widget title;
final List<Widget> children;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: children,
),
),
],
),
);
}
}

119
lib/widgets/user_login.dart Normal file
View file

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lottie/lottie.dart';
import 'package:whispering_pages/hacks/fix_autofill_losing_focus.dart';
class UserLogin extends HookConsumerWidget {
UserLogin({
super.key,
this.usernameController,
this.passwordController,
this.onPressed,
});
TextEditingController? usernameController;
TextEditingController? passwordController;
final void Function()? onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
usernameController ??= useTextEditingController();
passwordController ??= useTextEditingController();
final isPasswordVisibleAnimationController = useAnimationController(
duration: const Duration(milliseconds: 500),
);
var isPasswordVisible = useState(false);
// forward animation when the password visibility changes
useEffect(
() {
if (isPasswordVisible.value) {
isPasswordVisibleAnimationController.forward();
} else {
isPasswordVisibleAnimationController.reverse();
}
return null;
},
[isPasswordVisible.value],
);
return Center(
child: InactiveFocusScopeObserver(
child: AutofillGroup(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: usernameController,
autofocus: true,
autofillHints: const [AutofillHints.username],
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: 'Username',
labelStyle: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.8),
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: passwordController,
autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.done,
obscureText: !isPasswordVisible.value,
onFieldSubmitted: onPressed != null
? (_) {
onPressed!();
}
: null,
decoration: InputDecoration(
labelText: 'Password',
labelStyle: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.8),
),
border: const OutlineInputBorder(),
suffixIcon: ColorFiltered(
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary.withOpacity(0.8),
BlendMode.srcIn,
),
child: InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () {
isPasswordVisible.value = !isPasswordVisible.value;
},
child: Container(
margin: const EdgeInsets.only(left: 8, right: 8),
child: Lottie.asset(
'assets/animations/Animation - 1714930099660.json',
controller: isPasswordVisibleAnimationController,
),
),
),
),
suffixIconConstraints: const BoxConstraints(
maxHeight: 45,
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: onPressed,
child: const Text('Login'),
),
],
),
),
),
);
}
}

1
linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

145
linux/CMakeLists.txt Normal file
View file

@ -0,0 +1,145 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "whispering_pages")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.whispering_pages")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
isar_flutter_libs
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

6
linux/main.cc Normal file
View file

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

124
linux/my_application.cc Normal file
View file

@ -0,0 +1,124 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "whispering_pages");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "whispering_pages");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

18
linux/my_application.h Normal file
View file

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

1012
pubspec.lock Normal file

File diff suppressed because it is too large Load diff

107
pubspec.yaml Normal file
View file

@ -0,0 +1,107 @@
name: whispering_pages
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ">=3.3.4 <4.0.0"
isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
animated_list_plus: ^0.5.2
animated_theme_switcher: ^2.0.10
auto_scroll_text: ^0.0.7
cached_network_image: ^3.3.1
coast: ^2.0.2
collection: ^1.18.0
cupertino_icons: ^1.0.6
easy_stepper: ^0.8.4
flutter:
sdk: flutter
flutter_cache_manager: ^3.3.2
flutter_hooks: ^0.20.5
flutter_settings_ui: ^3.0.1
freezed_annotation: ^2.4.1
hive: ^4.0.0-dev.2
hooks_riverpod: ^2.5.1
isar: *isar_version
isar_flutter_libs: *isar_version # contains Isar Core
json_annotation: ^4.9.0
lottie: ^3.1.0
path: ^1.9.0
path_provider: ^2.1.0
riverpod_annotation: ^2.3.5
scroll_loop_auto_scroll: ^0.0.5
shelfsdk:
path: ../../_dart/shelfsdk
dev_dependencies:
build_runner: ^2.4.9
custom_lint: ^0.6.4
flutter_lints: ^3.0.0
flutter_test:
sdk: flutter
freezed: ^2.5.2
json_serializable: ^6.8.0
riverpod_generator: ^2.4.0
riverpod_lint: ^2.3.10
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- assets/
- assets/animations/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

BIN
web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

BIN
web/icons/Icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
web/icons/Icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

59
web/index.html Normal file
View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="whispering_pages">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>whispering_pages</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body>
</html>

35
web/manifest.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "whispering_pages",
"short_name": "whispering_pages",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

17
windows/.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

108
windows/CMakeLists.txt Normal file
View file

@ -0,0 +1,108 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(whispering_pages LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "whispering_pages")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View file

@ -0,0 +1,109 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

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