import 'package:flutter/foundation.dart'; import 'media_playback.dart'; /// One track in the native Jellyfin playback queue. @immutable class JellyfinQueueItem { const JellyfinQueueItem({ required this.trackId, required this.mediaSourceId, required this.url, required this.title, required this.artist, required this.album, required this.durationMs, required this.index, }); final String trackId; final String mediaSourceId; final String url; final String title; final String artist; final String album; final int durationMs; final int index; Map toMap() => { 'trackId': trackId, 'mediaSourceId': mediaSourceId, 'url': url, 'title': title, 'artist': artist, 'album': album, 'durationMs': durationMs, 'index': index, }; } /// Everything the native service needs to start one album. [authorization] and /// [accessToken] are kept in memory and are never logged or persisted. @immutable class JellyfinPlaybackQueue { const JellyfinPlaybackQueue({ required this.baseUrl, required this.authorization, required this.accessToken, required this.userId, required this.deviceId, required this.playSessionId, required this.albumId, required this.albumName, required this.artistName, required this.items, this.artwork, }); final Uri baseUrl; final String authorization; final String accessToken; final String userId; final String deviceId; final String playSessionId; final String albumId; final String albumName; final String artistName; final List items; final Uint8List? artwork; Map toMap() => { 'baseUrl': baseUrl.toString(), 'authorization': authorization, 'accessToken': accessToken, 'userId': userId, 'deviceId': deviceId, 'playSessionId': playSessionId, 'albumId': albumId, 'albumName': albumName, 'artistName': artistName, 'artwork': artwork, 'items': items.map((item) => item.toMap()).toList(growable: false), }; } /// The native player's current state. Positions are milliseconds. @immutable class JellyfinPlaybackSnapshot { const JellyfinPlaybackSnapshot({ this.available = false, this.playing = false, this.buffering = false, this.itemId, this.albumId, this.title, this.artist, this.album, this.durationMs = 0, this.positionMs = 0, this.index = 0, this.hasPrevious = false, this.hasNext = false, this.error, }); static const empty = JellyfinPlaybackSnapshot(); final bool available; final bool playing; final bool buffering; final String? itemId; final String? albumId; final String? title; final String? artist; final String? album; final int durationMs; final int positionMs; final int index; final bool hasPrevious; final bool hasNext; final String? error; bool get playingOrBuffering => playing || buffering; bool get ended => available && !playing && durationMs > 0 && positionMs >= durationMs - 500; double get progress { if (durationMs <= 0) return 0; return (positionMs / durationMs).clamp(0.0, 1.0); } JellyfinPlaybackSnapshot copyWith({ bool? available, bool? playing, bool? buffering, int? positionMs, int? index, bool? hasPrevious, bool? hasNext, String? error, }) { return JellyfinPlaybackSnapshot( available: available ?? this.available, playing: playing ?? this.playing, buffering: buffering ?? this.buffering, itemId: itemId, albumId: albumId, title: title, artist: artist, album: album, durationMs: durationMs, positionMs: positionMs ?? this.positionMs, index: index ?? this.index, hasPrevious: hasPrevious ?? this.hasPrevious, hasNext: hasNext ?? this.hasNext, error: error ?? this.error, ); } factory JellyfinPlaybackSnapshot.fromMap(Map map) { int integer(String key) => (map[key] as num?)?.toInt() ?? 0; String? string(String key) { final value = (map[key] as String?)?.trim(); return value == null || value.isEmpty ? null : value; } return JellyfinPlaybackSnapshot( available: map['available'] == true, playing: map['playing'] == true, buffering: map['buffering'] == true, itemId: string('itemId'), albumId: string('albumId'), title: string('title'), artist: string('artist'), album: string('album'), durationMs: integer('durationMs'), positionMs: integer('positionMs'), index: integer('index'), hasPrevious: map['hasPrevious'] == true, hasNext: map['hasNext'] == true, error: string('error'), ); } /// Snapshot consumed by the Home volume card and the existing media widgets. MediaPlayback toMediaPlayback() { return MediaPlayback( hasAccess: available, hasSession: available && itemId != null, playing: playing, title: title, artist: artist, packageName: 'com.wasp.cardash_launcher', durationMs: durationMs, positionMs: positionMs, ); } @override bool operator ==(Object other) { return other is JellyfinPlaybackSnapshot && other.available == available && other.playing == playing && other.buffering == buffering && other.itemId == itemId && other.albumId == albumId && other.title == title && other.artist == artist && other.album == album && other.durationMs == durationMs && other.positionMs == positionMs && other.index == index && other.hasPrevious == hasPrevious && other.hasNext == hasNext && other.error == error; } @override int get hashCode => Object.hash( available, playing, buffering, itemId, albumId, title, artist, album, durationMs, positionMs, index, hasPrevious, hasNext, error, ); }