From 37c44f1c6b2ac33e984cfd7b48614f1d34322ba6 Mon Sep 17 00:00:00 2001
From: "Dr.Blank" <64108942+Dr-Blank@users.noreply.github.com>
Date: Fri, 11 Apr 2025 21:00:50 +0530
Subject: [PATCH 1/2] add more ways to get app
---
README.md | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index 0df2e00..869bc3f 100644
--- a/README.md
+++ b/README.md
@@ -18,24 +18,20 @@ Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server ma
### Android
-[
](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-release-universal.apk) [
](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani)
+[
](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani)
+[
](https://play.google.com/store/apps/details?id=dr.blank.vaani)
+[
](https://apt.izzysoft.de/fdroid/index/apk/dr.blank.vaani)
+[
](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-universal-release.apk)
-Playstore App is in closed testing. To join testing
-1. [Join the Google Group](https://groups.google.com/g/vaani-app)
-2. [Join on Web](https://play.google.com/apps/testing/dr.blank.vaani) (preferred) or [Join on Android](https://play.google.com/store/apps/details?id=dr.blank.vaani) by becoming a tester.
- - Make sure it is the same account as the one you joined group.
-3. Send me an email on drblankdev@gmail.com asking for a promo code.
- - I only have limited codes, so first come first serve.
-4. Redeem the code on Playstore and install the app.
-
+*Play Store version is paid if you want to support the development.*
## Screencaps
https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d
-|
|
|
|
-|:---:|:---:|:---:|
-|Home|Book View|Player|
+|
|
|
|
+| :-----------------------------------------------------------: | :---------------------------------------------------------------: | :-------------------------------------------------------------: |
+| Home | Book View | Player |
Currently, the app is in development and is not ready for production use.
From e21977b894441448d1a4b5c5fba86e03992817ee Mon Sep 17 00:00:00 2001
From: "Dr.Blank" <64108942+Dr-Blank@users.noreply.github.com>
Date: Fri, 18 Apr 2025 12:38:51 +0530
Subject: [PATCH 2/2] feat: implement library selection in YouPage
---
lib/api/library_provider.dart | 42 +++++
lib/api/library_provider.g.dart | 176 +++++++++++++++++++
lib/features/logging/view/logs_page.dart | 4 -
lib/features/you/view/you_page.dart | 210 ++++++++++++++++++++---
lib/shared/icons/abs_icons.dart | 1 +
lib/shared/widgets/vaani_logo.dart | 27 +++
shelfsdk | 2 +-
7 files changed, 430 insertions(+), 32 deletions(-)
create mode 100644 lib/api/library_provider.dart
create mode 100644 lib/api/library_provider.g.dart
create mode 100644 lib/shared/widgets/vaani_logo.dart
diff --git a/lib/api/library_provider.dart b/lib/api/library_provider.dart
new file mode 100644
index 0000000..a138a7a
--- /dev/null
+++ b/lib/api/library_provider.dart
@@ -0,0 +1,42 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref;
+import 'package:logging/logging.dart' show Logger;
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
+import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider;
+part 'library_provider.g.dart';
+
+final _logger = Logger('LibraryProvider');
+
+@riverpod
+Future currentLibrary(Ref ref, String id) async {
+ final api = ref.watch(authenticatedApiProvider);
+ final library = await api.libraries.get(libraryId: id);
+ if (library == null) {
+ _logger.warning('No library found through id: $id');
+ // try to get the library from the list of libraries
+ final libraries = await ref.watch(librariesProvider.future);
+ for (final lib in libraries) {
+ if (lib.id == id) {
+ return lib;
+ }
+ }
+ _logger.warning('No library found in the list of libraries');
+ return null;
+ }
+ return library.library;
+}
+
+@riverpod
+class Libraries extends _$Libraries {
+ @override
+ FutureOr> build() async {
+ final api = ref.watch(authenticatedApiProvider);
+ final libraries = await api.libraries.getAll();
+ if (libraries == null) {
+ _logger.warning('Failed to fetch libraries');
+ return [];
+ }
+ return libraries;
+ }
+}
diff --git a/lib/api/library_provider.g.dart b/lib/api/library_provider.g.dart
new file mode 100644
index 0000000..3161d34
--- /dev/null
+++ b/lib/api/library_provider.g.dart
@@ -0,0 +1,176 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'library_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$currentLibraryHash() => r'f37904b8b43c88a523696d1ed7acf871c3e3326f';
+
+/// 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));
+ }
+}
+
+/// See also [currentLibrary].
+@ProviderFor(currentLibrary)
+const currentLibraryProvider = CurrentLibraryFamily();
+
+/// See also [currentLibrary].
+class CurrentLibraryFamily extends Family> {
+ /// See also [currentLibrary].
+ const CurrentLibraryFamily();
+
+ /// See also [currentLibrary].
+ CurrentLibraryProvider call(
+ String id,
+ ) {
+ return CurrentLibraryProvider(
+ id,
+ );
+ }
+
+ @override
+ CurrentLibraryProvider getProviderOverride(
+ covariant CurrentLibraryProvider provider,
+ ) {
+ return call(
+ provider.id,
+ );
+ }
+
+ static const Iterable? _dependencies = null;
+
+ @override
+ Iterable? get dependencies => _dependencies;
+
+ static const Iterable? _allTransitiveDependencies = null;
+
+ @override
+ Iterable? get allTransitiveDependencies =>
+ _allTransitiveDependencies;
+
+ @override
+ String? get name => r'currentLibraryProvider';
+}
+
+/// See also [currentLibrary].
+class CurrentLibraryProvider extends AutoDisposeFutureProvider {
+ /// See also [currentLibrary].
+ CurrentLibraryProvider(
+ String id,
+ ) : this._internal(
+ (ref) => currentLibrary(
+ ref as CurrentLibraryRef,
+ id,
+ ),
+ from: currentLibraryProvider,
+ name: r'currentLibraryProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$currentLibraryHash,
+ dependencies: CurrentLibraryFamily._dependencies,
+ allTransitiveDependencies:
+ CurrentLibraryFamily._allTransitiveDependencies,
+ id: id,
+ );
+
+ CurrentLibraryProvider._internal(
+ super._createNotifier, {
+ required super.name,
+ required super.dependencies,
+ required super.allTransitiveDependencies,
+ required super.debugGetCreateSourceHash,
+ required super.from,
+ required this.id,
+ }) : super.internal();
+
+ final String id;
+
+ @override
+ Override overrideWith(
+ FutureOr Function(CurrentLibraryRef provider) create,
+ ) {
+ return ProviderOverride(
+ origin: this,
+ override: CurrentLibraryProvider._internal(
+ (ref) => create(ref as CurrentLibraryRef),
+ from: from,
+ name: null,
+ dependencies: null,
+ allTransitiveDependencies: null,
+ debugGetCreateSourceHash: null,
+ id: id,
+ ),
+ );
+ }
+
+ @override
+ AutoDisposeFutureProviderElement createElement() {
+ return _CurrentLibraryProviderElement(this);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is CurrentLibraryProvider && other.id == id;
+ }
+
+ @override
+ int get hashCode {
+ var hash = _SystemHash.combine(0, runtimeType.hashCode);
+ hash = _SystemHash.combine(hash, id.hashCode);
+
+ return _SystemHash.finish(hash);
+ }
+}
+
+@Deprecated('Will be removed in 3.0. Use Ref instead')
+// ignore: unused_element
+mixin CurrentLibraryRef on AutoDisposeFutureProviderRef {
+ /// The parameter `id` of this provider.
+ String get id;
+}
+
+class _CurrentLibraryProviderElement
+ extends AutoDisposeFutureProviderElement with CurrentLibraryRef {
+ _CurrentLibraryProviderElement(super.provider);
+
+ @override
+ String get id => (origin as CurrentLibraryProvider).id;
+}
+
+String _$librariesHash() => r'a79954d0b68a8265859c577e36d5596620a72843';
+
+/// See also [Libraries].
+@ProviderFor(Libraries)
+final librariesProvider =
+ AutoDisposeAsyncNotifierProvider>.internal(
+ Libraries.new,
+ name: r'librariesProvider',
+ debugGetCreateSourceHash:
+ const bool.fromEnvironment('dart.vm.product') ? null : _$librariesHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$Libraries = AutoDisposeAsyncNotifier>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
diff --git a/lib/features/logging/view/logs_page.dart b/lib/features/logging/view/logs_page.dart
index ad2c764..b774963 100644
--- a/lib/features/logging/view/logs_page.dart
+++ b/lib/features/logging/view/logs_page.dart
@@ -1,15 +1,11 @@
-import 'dart:io';
-import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
-import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
import 'package:vaani/features/logging/providers/logs_provider.dart';
import 'package:vaani/main.dart';
-import 'package:vaani/settings/metadata/metadata_provider.dart';
class LogsPage extends HookConsumerWidget {
const LogsPage({super.key});
diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart
index c15e80f..69eeecc 100644
--- a/lib/features/you/view/you_page.dart
+++ b/lib/features/you/view/you_page.dart
@@ -1,12 +1,22 @@
+import 'dart:io' show Platform;
+
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
import 'package:vaani/api/api_provider.dart';
+import 'package:vaani/api/library_provider.dart' show librariesProvider;
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
+import 'package:vaani/main.dart' show appLogger;
import 'package:vaani/router/router.dart';
+import 'package:vaani/settings/api_settings_provider.dart'
+ show apiSettingsProvider;
import 'package:vaani/settings/constants.dart';
+import 'package:vaani/shared/icons/abs_icons.dart';
import 'package:vaani/shared/utils.dart';
import 'package:vaani/shared/widgets/not_implemented.dart';
+import 'package:vaani/shared/widgets/vaani_logo.dart';
class YouPage extends HookConsumerWidget {
const YouPage({
@@ -16,6 +26,9 @@ class YouPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final api = ref.watch(authenticatedApiProvider);
+ final librariesAsyncValue = ref.watch(librariesProvider);
+ // Get current settings to know the active library ID later
+ final apiSettings = ref.watch(apiSettingsProvider);
return Scaffold(
appBar: AppBar(
// title: const Text('You'),
@@ -63,7 +76,58 @@ class YouPage extends HookConsumerWidget {
context.pushNamed(Routes.userManagement.name);
},
),
- // ActionChip(
+ librariesAsyncValue.when(
+ data: (libraries) => ActionChip(
+ avatar: Icon(
+ AbsIcons.getIconByName(
+ apiSettings.activeLibraryId != null
+ ? libraries
+ .firstWhere(
+ (lib) =>
+ lib.id ==
+ apiSettings.activeLibraryId,
+ )
+ .icon
+ : libraries.first.icon,
+ ),
+ ), // Replace with your icon
+ label: const Text('Change Library'),
+ // Enable only if libraries are loaded and not empty
+ onPressed: libraries.isNotEmpty
+ ? () => _showLibrarySwitcher(
+ context,
+ ref,
+ libraries,
+ apiSettings.activeLibraryId,
+ )
+ : null, // Disable if no libraries
+ ),
+ loading: () => const ActionChip(
+ avatar: SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ ),
+ label: Text('Loading Libs...'),
+ onPressed: null, // Disable while loading
+ ),
+ error: (error, stack) => ActionChip(
+ avatar: Icon(
+ Icons.error_outline,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ label: const Text('Error Loading Libs'),
+ onPressed: () {
+ // Maybe show error details or allow retry
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content:
+ Text('Failed to load libraries: $error'),
+ ),
+ );
+ },
+ ),
+ ), // ActionChip(
// avatar: const Icon(Icons.logout),
// label: const Text('Logout'),
// onPressed: () {
@@ -144,6 +208,124 @@ class YouPage extends HookConsumerWidget {
}
}
+// --- Helper Function to Show the Switcher ---
+void _showLibrarySwitcher(
+ BuildContext context,
+ WidgetRef ref,
+ List libraries, // Pass loaded libraries
+ String? currentLibraryId, // Pass current ID
+) {
+ final content = _LibrarySelectionContent(
+ libraries: libraries,
+ currentLibraryId: currentLibraryId,
+ );
+
+ // --- Platform-Specific UI ---
+ bool isDesktop = false;
+ if (!kIsWeb) {
+ // dart:io Platform is not available on web
+ isDesktop = Platform.isLinux || Platform.isMacOS || Platform.isWindows;
+ } else {
+ // Basic web detection (might need refinement based on screen size)
+ // Consider using MediaQuery for a size-based check instead for web/tablet
+ final size = MediaQuery.of(context).size;
+ isDesktop = size.width > 600; // Example threshold for "desktop-like" layout
+ }
+
+ if (isDesktop) {
+ // --- Desktop: Use AlertDialog ---
+ showDialog(
+ context: context,
+ builder: (dialogContext) => AlertDialog(
+ title: const Text('Select Library'),
+ content: SizedBox(
+ // Constrain size for dialogs
+ width: 300, // Adjust as needed
+ // Make content scrollable if list is long
+ child: Scrollbar(child: content),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(dialogContext),
+ child: const Text('Cancel'),
+ ),
+ ],
+ ),
+ );
+ } else {
+ // --- Mobile/Tablet: Use BottomSheet ---
+ showModalBottomSheet(
+ context: context,
+ // Make it scrollable and control height
+ isScrollControlled: true,
+ constraints: BoxConstraints(
+ maxHeight:
+ MediaQuery.of(context).size.height * 0.6, // Max 60% of screen
+ ),
+ builder: (sheetContext) => Padding(
+ // Add padding within the bottom sheet
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min, // Take minimum necessary height
+ children: [
+ const Text(
+ 'Select Library',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 10),
+ const Divider(),
+ Flexible(
+ // Allow the list to take remaining space and scroll
+ child: Scrollbar(child: content),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+// --- Widget for the Selection List Content (Reusable) ---
+class _LibrarySelectionContent extends ConsumerWidget {
+ final List libraries;
+ final String? currentLibraryId;
+
+ const _LibrarySelectionContent({
+ required this.libraries,
+ this.currentLibraryId,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ return ListView.builder(
+ shrinkWrap: true, // Important for Dialog/BottomSheet sizing
+ itemCount: libraries.length,
+ itemBuilder: (context, index) {
+ final library = libraries[index];
+ final bool isSelected = library.id == currentLibraryId;
+
+ return ListTile(
+ title: Text(library.name),
+ leading: Icon(AbsIcons.getIconByName(library.icon)),
+ selected: isSelected, // Makes the tile visually selected
+ onTap: () {
+ appLogger
+ .info('Selected library: ${library.name} (ID: ${library.id})');
+ // Get current settings state
+ final currentSettings = ref.read(apiSettingsProvider);
+ // Update the active library ID
+ ref.read(apiSettingsProvider.notifier).updateState(
+ currentSettings.copyWith(activeLibraryId: library.id),
+ );
+ // Close the dialog/bottom sheet
+ Navigator.pop(context);
+ },
+ );
+ },
+ );
+ }
+}
+
class UserBar extends HookConsumerWidget {
const UserBar({
super.key,
@@ -185,29 +367,3 @@ class UserBar extends HookConsumerWidget {
);
}
}
-
-class VaaniLogo extends StatelessWidget {
- const VaaniLogo({
- super.key,
- this.size,
- this.duration = const Duration(milliseconds: 750),
- this.curve = Curves.fastOutSlowIn,
- });
-
- final double? size;
- final Duration duration;
- final Curve curve;
-
- @override
- Widget build(BuildContext context) {
- final IconThemeData iconTheme = IconTheme.of(context);
- final double? iconSize = size ?? iconTheme.size;
- return AnimatedContainer(
- width: iconSize,
- height: iconSize,
- duration: duration,
- curve: curve,
- child: Image.asset('assets/images/vaani_logo_foreground.png'),
- );
- }
-}
diff --git a/lib/shared/icons/abs_icons.dart b/lib/shared/icons/abs_icons.dart
index f112d0b..eca3e1a 100644
--- a/lib/shared/icons/abs_icons.dart
+++ b/lib/shared/icons/abs_icons.dart
@@ -13,6 +13,7 @@
///
///
///
+library;
// ignore_for_file: constant_identifier_names
import 'package:flutter/widgets.dart' show IconData;
diff --git a/lib/shared/widgets/vaani_logo.dart b/lib/shared/widgets/vaani_logo.dart
new file mode 100644
index 0000000..a5b4e11
--- /dev/null
+++ b/lib/shared/widgets/vaani_logo.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+
+class VaaniLogo extends StatelessWidget {
+ const VaaniLogo({
+ super.key,
+ this.size,
+ this.duration = const Duration(milliseconds: 750),
+ this.curve = Curves.fastOutSlowIn,
+ });
+
+ final double? size;
+ final Duration duration;
+ final Curve curve;
+
+ @override
+ Widget build(BuildContext context) {
+ final IconThemeData iconTheme = IconTheme.of(context);
+ final double? iconSize = size ?? iconTheme.size;
+ return AnimatedContainer(
+ width: iconSize,
+ height: iconSize,
+ duration: duration,
+ curve: curve,
+ child: Image.asset('assets/images/vaani_logo_foreground.png'),
+ );
+ }
+}
diff --git a/shelfsdk b/shelfsdk
index 5cc545c..e1848a4 160000
--- a/shelfsdk
+++ b/shelfsdk
@@ -1 +1 @@
-Subproject commit 5cc545ca87c05615473ab9c363cfa29e341d1e2a
+Subproject commit e1848a42c27257146015a33e9427f197f522fe03