summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-20 23:34:37 +0300
committerPaul Buetow <paul@buetow.org>2026-05-20 23:34:37 +0300
commit1a68b1aead492b50d824bde99fb1f30e1360ed26 (patch)
tree651c86fa2d81758e5ee6cf124aa72cc0587aff36
parent30e95cd41fc26cc2cbef658c47c690eb5e388b04 (diff)
Add DioClient with auth interceptors and refactor PlayerApiClient (ma)
Introduces dio_client.dart with _AuthInterceptor (guards Bearer token injection with containsKey so callers can override Authorization) and _UnauthorizedInterceptor (private fields, 401 → login redirect). PlayerApiClient is refactored to accept a pre-configured Dio instance instead of raw credentials; adds dio + flutter_secure_storage deps. Fixes review issues: Auth header comment/behavior corrected, private fields on _UnauthorizedInterceptor, unused import removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--player-android/lib/api/dio_client.dart152
-rw-r--r--player-android/lib/api/player_api_client.dart269
-rw-r--r--player-android/pubspec.lock321
-rw-r--r--player-android/pubspec.yaml4
-rw-r--r--player-android/test/models_test.dart55
5 files changed, 744 insertions, 57 deletions
diff --git a/player-android/lib/api/dio_client.dart b/player-android/lib/api/dio_client.dart
new file mode 100644
index 0000000..35db589
--- /dev/null
+++ b/player-android/lib/api/dio_client.dart
@@ -0,0 +1,152 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+
+// Storage key under which the bearer token is persisted across app restarts.
+const _kTokenKey = 'bearer_token';
+
+/// Abstraction over the secure token store so the interceptor can be tested
+/// without platform code (Liskov substitution / dependency inversion).
+abstract interface class TokenStorage {
+ Future<String?> readToken();
+ Future<void> writeToken(String token);
+ Future<void> deleteToken();
+}
+
+/// Production implementation backed by [FlutterSecureStorage].
+///
+/// Uses AES encryption on Android and the iOS Keychain on Apple platforms.
+class SecureTokenStorage implements TokenStorage {
+ SecureTokenStorage({FlutterSecureStorage? storage})
+ : _storage = storage ?? const FlutterSecureStorage();
+
+ final FlutterSecureStorage _storage;
+
+ @override
+ Future<String?> readToken() => _storage.read(key: _kTokenKey);
+
+ @override
+ Future<void> writeToken(String token) =>
+ _storage.write(key: _kTokenKey, value: token);
+
+ @override
+ Future<void> deleteToken() => _storage.delete(key: _kTokenKey);
+}
+
+/// Interceptor that attaches a Bearer token to every outgoing request.
+///
+/// The token is read lazily from [TokenStorage] so that changes (login /
+/// logout) are picked up without restarting the Dio instance.
+class _AuthInterceptor extends Interceptor {
+ _AuthInterceptor(this._storage);
+
+ final TokenStorage _storage;
+
+ @override
+ Future<void> onRequest(
+ RequestOptions options,
+ RequestInterceptorHandler handler,
+ ) async {
+ final token = await _storage.readToken();
+ if (token != null && token.isNotEmpty) {
+ // Only attach the bearer token when no Authorization header has been set
+ // explicitly by the caller (e.g. public endpoints may supply their own
+ // credentials and must not be overwritten).
+ if (!options.headers.containsKey('Authorization')) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+ }
+ handler.next(options);
+ }
+}
+
+/// Interceptor that intercepts 401 Unauthorized responses and redirects the
+/// user to the login route via the supplied [NavigatorKey].
+///
+/// On 401, the stored token is removed (it is no longer valid) and the
+/// navigator pushes a named replacement so that the back-stack cannot return
+/// the user to an authenticated screen.
+class _UnauthorizedInterceptor extends Interceptor {
+ _UnauthorizedInterceptor({
+ required TokenStorage storage,
+ required GlobalKey<NavigatorState> navigatorKey,
+ String loginRoute = '/login',
+ }) : _storage = storage,
+ _navigatorKey = navigatorKey,
+ _loginRoute = loginRoute;
+
+ // Private fields consistent with _AuthInterceptor naming conventions.
+ final TokenStorage _storage;
+ final GlobalKey<NavigatorState> _navigatorKey;
+ final String _loginRoute;
+
+ @override
+ Future<void> onError(
+ DioException err,
+ ErrorInterceptorHandler handler,
+ ) async {
+ if (err.response?.statusCode == 401) {
+ // Purge the stale token so subsequent requests start unauthenticated.
+ await _storage.deleteToken();
+
+ // Use the navigator key to redirect without needing a BuildContext.
+ _navigatorKey.currentState
+ ?.pushNamedAndRemoveUntil(_loginRoute, (_) => false);
+ }
+ handler.next(err);
+ }
+}
+
+/// Factory that assembles a fully configured [Dio] instance wired with:
+/// - bearer-token injection on every request, and
+/// - global 401 → login redirect.
+///
+/// Callers own the returned [Dio] and may add further interceptors on top.
+/// Separating construction from usage (SRP) keeps this class testable.
+class DioClient {
+ DioClient({
+ required Uri baseUrl,
+ required TokenStorage storage,
+ required GlobalKey<NavigatorState> navigatorKey,
+ String loginRoute = '/login',
+ BaseOptions? baseOptions,
+ }) : _dio = _buildDio(
+ baseUrl: baseUrl,
+ storage: storage,
+ navigatorKey: navigatorKey,
+ loginRoute: loginRoute,
+ baseOptions: baseOptions,
+ );
+
+ final Dio _dio;
+
+ /// Exposes the underlying [Dio] so that [PlayerApiClient] can issue typed
+ /// requests without re-implementing the interceptor plumbing.
+ Dio get dio => _dio;
+
+ static Dio _buildDio({
+ required Uri baseUrl,
+ required TokenStorage storage,
+ required GlobalKey<NavigatorState> navigatorKey,
+ required String loginRoute,
+ BaseOptions? baseOptions,
+ }) {
+ final options = (baseOptions ?? BaseOptions()).copyWith(
+ baseUrl: baseUrl.toString(),
+ // JSON is the wire format for all API endpoints.
+ contentType: 'application/json',
+ responseType: ResponseType.json,
+ );
+
+ return Dio(options)
+ ..interceptors.addAll([
+ // Auth must run before the 401 handler so the token is attached first.
+ _AuthInterceptor(storage),
+ _UnauthorizedInterceptor(
+ storage: storage,
+ navigatorKey: navigatorKey,
+ loginRoute: loginRoute,
+ ),
+ ]);
+ }
+}
diff --git a/player-android/lib/api/player_api_client.dart b/player-android/lib/api/player_api_client.dart
index eb438f4..9e987c2 100644
--- a/player-android/lib/api/player_api_client.dart
+++ b/player-android/lib/api/player_api_client.dart
@@ -1,64 +1,269 @@
import 'dart:typed_data';
+import 'package:dio/dio.dart';
+
import '../models/models.dart';
+/// High-level API surface that maps 1-to-1 with the player-server REST API
+/// (see player-server/docs/api.md for the authoritative contract).
+///
+/// Each method corresponds to a single HTTP endpoint. All HTTP plumbing
+/// (bearer-token injection, 401 → login redirect, base-URL configuration) is
+/// handled by the [Dio] instance provided at construction time.
+///
+/// In production, create the [Dio] via [DioClient] which wires up the auth
+/// and 401-redirect interceptors. In tests, pass a plain or mocked [Dio].
+///
+/// Concrete implementations of the stub methods will be added incrementally as
+/// features are built.
class PlayerApiClient {
- final Uri baseUrl;
- final String bearerToken;
+ /// Creates a client backed by [dio].
+ ///
+ /// Prefer creating [dio] via [DioClient] in production to get bearer-token
+ /// injection and 401 → login redirect out of the box.
+ PlayerApiClient({required Dio dio}) : _dio = dio;
+
+ // The configured Dio instance with auth/401 interceptors already applied.
+ final Dio _dio;
+
+ // ---------------------------------------------------------------------------
+ // Auth
+ // ---------------------------------------------------------------------------
- // Normal (non-const) constructor: Uri is not const-constructable, so the
- // constructor must not be declared const.
- PlayerApiClient({required this.baseUrl, required this.bearerToken});
+ Future<User> bootstrap({
+ required String username,
+ required String password,
+ }) =>
+ throw UnimplementedError();
+
+ Future<User> login({
+ required String username,
+ required String password,
+ }) =>
+ throw UnimplementedError();
- Future<User> bootstrap({required String username, required String password}) => throw UnimplementedError();
- Future<User> login({required String username, required String password}) => throw UnimplementedError();
Future<void> logout() => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Health
+ // ---------------------------------------------------------------------------
+
Future<void> healthz() => throw UnimplementedError();
Future<void> readyz() => throw UnimplementedError();
- Future<String> getSharedMediaPage(String token) => throw UnimplementedError();
- Future<Uint8List> streamSharedMedia(String token, {String? range}) => throw UnimplementedError();
- Future<Uint8List> getSharedThumbnail(String token) => throw UnimplementedError();
- Future<Uint8List> downloadSharedMedia(String token) => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Shared / public endpoints (no auth required)
+ // ---------------------------------------------------------------------------
+
+ Future<String> getSharedMediaPage(String token) =>
+ throw UnimplementedError();
+
+ Future<Uint8List> streamSharedMedia(String token, {String? range}) =>
+ throw UnimplementedError();
+
+ Future<Uint8List> getSharedThumbnail(String token) =>
+ throw UnimplementedError();
+
+ Future<Uint8List> downloadSharedMedia(String token) =>
+ throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Config
+ // ---------------------------------------------------------------------------
+
Future<Map<String, dynamic>> getConfig() => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Sets
+ // ---------------------------------------------------------------------------
+
Future<List<MediaSet>> listSets() => throw UnimplementedError();
- Future<Map<String, dynamic>> browseSet(int setId, {String? parent}) => throw UnimplementedError();
- Future<Uint8List> getSetCover(int setId, {String? folder}) => throw UnimplementedError();
- Future<void> updateSetCover(int setId, {String? folder}) => throw UnimplementedError();
- Future<Media> uploadToSet(int setId, {required String fileName, required List<int> bytes}) => throw UnimplementedError();
- Future<List<Media>> listMedia({String? search, int? setId, List<int>? setIds, String? type, bool? favorites, List<String>? tags, double? minDuration, double? maxDuration, int? fileSizeMin, int? fileSizeMax, String? sort, int? limit, int? offset, String? folder, String? parent}) => throw UnimplementedError();
+
+ Future<Map<String, dynamic>> browseSet(int setId, {String? parent}) =>
+ throw UnimplementedError();
+
+ Future<Uint8List> getSetCover(int setId, {String? folder}) =>
+ throw UnimplementedError();
+
+ Future<void> updateSetCover(int setId, {String? folder}) =>
+ throw UnimplementedError();
+
+ Future<Media> uploadToSet(
+ int setId, {
+ required String fileName,
+ required List<int> bytes,
+ }) =>
+ throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Media
+ // ---------------------------------------------------------------------------
+
+ Future<List<Media>> listMedia({
+ String? search,
+ int? setId,
+ List<int>? setIds,
+ String? type,
+ bool? favorites,
+ List<String>? tags,
+ double? minDuration,
+ double? maxDuration,
+ int? fileSizeMin,
+ int? fileSizeMax,
+ String? sort,
+ int? limit,
+ int? offset,
+ String? folder,
+ String? parent,
+ }) =>
+ throw UnimplementedError();
+
Future<Media> getMedia(int mediaId) => throw UnimplementedError();
- Future<Uint8List> streamMedia(int mediaId, {String? range}) => throw UnimplementedError();
+
+ Future<Uint8List> streamMedia(int mediaId, {String? range}) =>
+ throw UnimplementedError();
+
Future<Uint8List> downloadMedia(int mediaId) => throw UnimplementedError();
+
Future<Uint8List> getThumbnail(int mediaId) => throw UnimplementedError();
+
Future<void> regenerateThumbnail(int mediaId) => throw UnimplementedError();
+
Future<bool> toggleFavorite(int mediaId) => throw UnimplementedError();
+
+ Future<void> deleteMedia(int mediaId) => throw UnimplementedError();
+
+ Future<void> restoreMedia(int mediaId) => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Tags
+ // ---------------------------------------------------------------------------
+
Future<List<Tag>> listTags() => throw UnimplementedError();
+
Future<void> addTag(int mediaId, String tag) => throw UnimplementedError();
- Future<void> removeTag(int mediaId, String tag) => throw UnimplementedError();
- Future<Share> createShare(int mediaId, {DateTime? expiresAt, int? maxUses}) => throw UnimplementedError();
- Future<List<Share>> listSharesForMedia(int mediaId) => throw UnimplementedError();
+
+ Future<void> removeTag(int mediaId, String tag) =>
+ throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Shares
+ // ---------------------------------------------------------------------------
+
+ Future<Share> createShare(
+ int mediaId, {
+ DateTime? expiresAt,
+ int? maxUses,
+ }) =>
+ throw UnimplementedError();
+
+ Future<List<Share>> listSharesForMedia(int mediaId) =>
+ throw UnimplementedError();
+
Future<List<Share>> listMyShares() => throw UnimplementedError();
+
Future<void> revokeShare(String token) => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Notes
+ // ---------------------------------------------------------------------------
+
Future<Note?> getNote(int mediaId) => throw UnimplementedError();
- Future<Note> upsertNote(int mediaId, String content) => throw UnimplementedError();
+
+ Future<Note> upsertNote(int mediaId, String content) =>
+ throw UnimplementedError();
+
Future<void> deleteNote(int mediaId) => throw UnimplementedError();
- Future<void> updateProgress({required int mediaId, required double positionSeconds}) => throw UnimplementedError();
- Future<void> updateProgressStatus({required int mediaId, required String status}) => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Progress
+ // ---------------------------------------------------------------------------
+
+ Future<void> updateProgress({
+ required int mediaId,
+ required double positionSeconds,
+ }) =>
+ throw UnimplementedError();
+
+ Future<void> updateProgressStatus({
+ required int mediaId,
+ required String status,
+ }) =>
+ throw UnimplementedError();
+
Future<List<Media>> listInProgress() => throw UnimplementedError();
- Future<void> deleteMedia(int mediaId) => throw UnimplementedError();
- Future<void> restoreMedia(int mediaId) => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Podcasts
+ // ---------------------------------------------------------------------------
+
Future<List<PodcastFeed>> listPodcasts() => throw UnimplementedError();
- Future<List<PodcastEpisode>> listEpisodes(int podcastSetId, {int? limit, int? offset}) => throw UnimplementedError();
+
+ Future<List<PodcastEpisode>> listEpisodes(
+ int podcastSetId, {
+ int? limit,
+ int? offset,
+ }) =>
+ throw UnimplementedError();
+
Future<Media> downloadEpisode(int episodeId) => throw UnimplementedError();
- Future<void> toggleEpisodeComplete(int episodeId) => throw UnimplementedError();
- Future<PodcastFeed> subscribePodcast({required String feedUrl, String? setName}) => throw UnimplementedError();
+
+ Future<void> toggleEpisodeComplete(int episodeId) =>
+ throw UnimplementedError();
+
+ Future<PodcastFeed> subscribePodcast({
+ required String feedUrl,
+ String? setName,
+ }) =>
+ throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Admin – Users
+ // ---------------------------------------------------------------------------
+
Future<List<User>> listUsers() => throw UnimplementedError();
- Future<User> createUser({required String username, required String password, required bool isAdmin}) => throw UnimplementedError();
+
+ Future<User> createUser({
+ required String username,
+ required String password,
+ required bool isAdmin,
+ }) =>
+ throw UnimplementedError();
+
Future<void> deleteUser(int userId) => throw UnimplementedError();
- Future<List<Map<String, dynamic>>> listPermissions() => throw UnimplementedError();
- Future<void> grantPermission({required int setId, required int userId, required String role}) => throw UnimplementedError();
- Future<void> revokePermission({required int setId, required int userId}) => throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Admin – Permissions
+ // ---------------------------------------------------------------------------
+
+ Future<List<Map<String, dynamic>>> listPermissions() =>
+ throw UnimplementedError();
+
+ Future<void> grantPermission({
+ required int setId,
+ required int userId,
+ required String role,
+ }) =>
+ throw UnimplementedError();
+
+ Future<void> revokePermission({
+ required int setId,
+ required int userId,
+ }) =>
+ throw UnimplementedError();
+
+ // ---------------------------------------------------------------------------
+ // Admin – Scanner
+ // ---------------------------------------------------------------------------
+
Future<void> triggerRescan() => throw UnimplementedError();
+
Future<Map<String, dynamic>> getScanProgress() => throw UnimplementedError();
+
Future<List<Media>> listTrash() => throw UnimplementedError();
+
+ // Expose the underlying Dio for advanced callers (e.g. binary streaming).
+ // This should not be used for ordinary JSON requests; prefer the typed
+ // methods above.
+ Dio get rawDio => _dio;
}
diff --git a/player-android/pubspec.lock b/player-android/pubspec.lock
index 47eee61..899bcb4 100644
--- a/player-android/pubspec.lock
+++ b/player-android/pubspec.lock
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
@@ -33,6 +41,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
+ code_assets:
+ dependency: transitive
+ description:
+ name: code_assets
+ sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
collection:
dependency: transitive
description:
@@ -41,6 +57,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.7"
+ dio:
+ dependency: "direct main"
+ description:
+ name: dio
+ sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.9.2"
+ dio_web_adapter:
+ dependency: transitive
+ description:
+ name: dio_web_adapter
+ sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
fake_async:
dependency: transitive
description:
@@ -49,6 +89,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
@@ -62,11 +118,112 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
+ flutter_secure_storage:
+ dependency: "direct main"
+ description:
+ name: flutter_secure_storage
+ sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.2.4"
+ flutter_secure_storage_linux:
+ dependency: transitive
+ description:
+ name: flutter_secure_storage_linux
+ sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.3"
+ flutter_secure_storage_macos:
+ dependency: transitive
+ description:
+ name: flutter_secure_storage_macos
+ sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
+ flutter_secure_storage_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_secure_storage_platform_interface
+ sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ flutter_secure_storage_web:
+ dependency: transitive
+ description:
+ name: flutter_secure_storage_web
+ sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ flutter_secure_storage_windows:
+ dependency: transitive
+ description:
+ name: flutter_secure_storage_windows
+ sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
+ flutter_web_plugins:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.3"
+ hooks:
+ dependency: transitive
+ description:
+ name: hooks
+ sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.2"
+ jni:
+ dependency: transitive
+ description:
+ name: jni
+ sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ jni_flutter:
+ dependency: transitive
+ description:
+ name: jni_flutter
+ sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.7"
leak_tracker:
dependency: transitive
description:
@@ -99,6 +256,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
matcher:
dependency: transitive
description:
@@ -123,6 +288,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ native_toolchain_c:
+ dependency: transitive
+ description:
+ name: native_toolchain_c
+ sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.17.6"
+ objective_c:
+ dependency: transitive
+ description:
+ name: objective_c
+ sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.3.0"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
path:
dependency: transitive
description:
@@ -131,6 +328,86 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
+ path_provider:
+ dependency: transitive
+ description:
+ name: path_provider
+ sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.5"
+ path_provider_android:
+ dependency: transitive
+ description:
+ name: path_provider_android
+ sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.1"
+ path_provider_foundation:
+ dependency: transitive
+ description:
+ name: path_provider_foundation
+ sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.6.0"
+ path_provider_linux:
+ dependency: transitive
+ description:
+ name: path_provider_linux
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
+ path_provider_platform_interface:
+ dependency: transitive
+ description:
+ name: path_provider_platform_interface
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ path_provider_windows:
+ dependency: transitive
+ description:
+ name: path_provider_windows
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.6"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.8"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
+ record_use:
+ dependency: transitive
+ description:
+ name: record_use
+ sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.0"
sky_engine:
dependency: transitive
description: flutter
@@ -184,6 +461,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.10"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
vector_math:
dependency: transitive
description:
@@ -200,6 +485,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.2.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.15.0"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.3"
sdks:
- dart: ">=3.9.0-0 <4.0.0"
- flutter: ">=3.18.0-18.0.pre.54"
+ dart: ">=3.10.3 <4.0.0"
+ flutter: ">=3.38.4"
diff --git a/player-android/pubspec.yaml b/player-android/pubspec.yaml
index 63c98ae..6464a8b 100644
--- a/player-android/pubspec.yaml
+++ b/player-android/pubspec.yaml
@@ -9,6 +9,10 @@ environment:
dependencies:
flutter:
sdk: flutter
+ # dio: type-safe HTTP client with interceptor support, used for all API calls.
+ dio: ^5.7.0
+ # flutter_secure_storage: stores the bearer token in the OS keychain/keystore.
+ flutter_secure_storage: ^9.2.2
dev_dependencies:
flutter_test:
diff --git a/player-android/test/models_test.dart b/player-android/test/models_test.dart
index 84fbf62..28f3a02 100644
--- a/player-android/test/models_test.dart
+++ b/player-android/test/models_test.dart
@@ -10,6 +10,7 @@
// DateTime.parse(s).toIso8601String() is lossless for UTC timestamps.
// Each model is also tested with all fields absent to verify defaults.
+import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:player_android/models/media.dart';
import 'package:player_android/models/media_set.dart';
@@ -527,37 +528,45 @@ void main() {
// PlayerApiClient constructor
// ---------------------------------------------------------------------------
group('PlayerApiClient constructor', () {
- test('stores baseUrl and bearerToken when valid', () {
- // The client stores the Uri and token as-is; no transformation occurs.
- final client = PlayerApiClient(
- baseUrl: Uri.parse('https://player.example.com'),
- bearerToken: 'my-secret-token',
- );
-
- expect(client.baseUrl.toString(), 'https://player.example.com');
- expect(client.bearerToken, 'my-secret-token');
+ test('can be constructed with a plain Dio instance', () {
+ // PlayerApiClient accepts any Dio instance so that tests do not need
+ // platform-specific secure storage or a real NavigatorKey.
+ final dio = Dio(BaseOptions(baseUrl: 'https://player.example.com'));
+ final client = PlayerApiClient(dio: dio);
+
+ // rawDio exposes the underlying instance for advanced use-cases.
+ expect(client.rawDio.options.baseUrl, 'https://player.example.com');
});
- test('baseUrl with path component is stored verbatim', () {
- // Verify the full URI including trailing path is preserved unchanged.
- final client = PlayerApiClient(
- baseUrl: Uri.parse('https://player.example.com/api/v1'),
- bearerToken: 'tok',
+ test('can be constructed with a Dio that has a path in the base URL', () {
+ // Verify the full base URL including path component is preserved.
+ final dio = Dio(
+ BaseOptions(baseUrl: 'https://player.example.com/api/v1'),
);
+ final client = PlayerApiClient(dio: dio);
- expect(client.baseUrl.path, '/api/v1');
- expect(client.baseUrl.host, 'player.example.com');
+ expect(
+ client.rawDio.options.baseUrl,
+ 'https://player.example.com/api/v1',
+ );
});
- test('stores an empty bearerToken unchanged', () {
- // A missing or empty token is allowed at construction time; the server
- // will reject unauthenticated requests at call time.
- final client = PlayerApiClient(
- baseUrl: Uri.parse('https://player.example.com'),
- bearerToken: '',
+ test('can be constructed with a Dio that carries a custom header', () {
+ // Confirms that any headers already configured on the Dio instance are
+ // preserved after construction (DioClient sets the auth header via an
+ // interceptor, but a test may set it directly on BaseOptions).
+ final dio = Dio(
+ BaseOptions(
+ baseUrl: 'https://player.example.com',
+ headers: {'Authorization': 'Bearer test-token'},
+ ),
);
+ final client = PlayerApiClient(dio: dio);
- expect(client.bearerToken, '');
+ expect(
+ client.rawDio.options.headers['Authorization'],
+ 'Bearer test-token',
+ );
});
});
}