Preserve Flutter mirror-os codebase

This commit is contained in:
Chris
2026-07-11 21:51:34 -05:00
commit 27631ed108
142 changed files with 11896 additions and 0 deletions
@@ -0,0 +1,6 @@
library mirror_protocol;
export 'src/messages.dart';
export 'src/commands.dart';
export 'src/state.dart';
export 'src/dashboard_config.dart';
@@ -0,0 +1,124 @@
enum ScreenType {
dashboard,
workout,
media,
standby;
static ScreenType fromJson(String value) =>
ScreenType.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum PlaybackAction {
play,
pause,
resume,
seek,
skip,
previous,
stop,
setVolume;
static PlaybackAction fromJson(String value) =>
PlaybackAction.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum WorkoutAction {
browse,
setFilter,
playVideo,
playFolder,
pauseWorkout,
resumeWorkout,
skipVideo,
skipRest,
previousVideo,
stopSession;
static WorkoutAction fromJson(String value) =>
WorkoutAction.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum MediaSource {
workout,
youtube,
plex,
local;
static MediaSource fromJson(String value) =>
MediaSource.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum WorkoutPhase {
idle,
browsing,
playing,
resting,
completed;
static WorkoutPhase fromJson(String value) =>
WorkoutPhase.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum WorkoutFilter {
all,
unwatched,
inProgress,
completed;
static WorkoutFilter fromJson(String value) =>
WorkoutFilter.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum ProgramAction {
browse,
playWorkout,
skipExercise,
previousExercise,
pauseExercise,
resumeExercise,
stopProgram;
static ProgramAction fromJson(String value) =>
ProgramAction.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum MediaAction {
searchYouTube,
playYouTube,
browsePlex,
playPlex,
spotifyPlay,
spotifyPause,
spotifyNext,
spotifyPrevious,
spotifyTransfer;
static MediaAction fromJson(String value) =>
MediaAction.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
enum DashboardAction {
refresh,
toggleWidget;
static DashboardAction fromJson(String value) =>
DashboardAction.values.firstWhere((e) => e.name == value);
String toJson() => name;
}
@@ -0,0 +1,74 @@
class DashboardWidgetConfig {
final String id;
final String type;
final bool visible;
final int position;
final Map<String, dynamic> config;
const DashboardWidgetConfig({
required this.id,
required this.type,
this.visible = true,
required this.position,
this.config = const {},
});
DashboardWidgetConfig copyWith({
String? id,
String? type,
bool? visible,
int? position,
Map<String, dynamic>? config,
}) =>
DashboardWidgetConfig(
id: id ?? this.id,
type: type ?? this.type,
visible: visible ?? this.visible,
position: position ?? this.position,
config: config ?? this.config,
);
factory DashboardWidgetConfig.fromJson(Map<String, dynamic> json) =>
DashboardWidgetConfig(
id: json['id'] as String,
type: json['type'] as String,
visible: json['visible'] as bool? ?? true,
position: json['position'] as int,
config: (json['config'] as Map<String, dynamic>?) ?? {},
);
Map<String, dynamic> toJson() => {
'id': id,
'type': type,
'visible': visible,
'position': position,
'config': config,
};
}
class DashboardLayoutConfig {
final List<DashboardWidgetConfig> widgets;
const DashboardLayoutConfig({required this.widgets});
factory DashboardLayoutConfig.defaultLayout() => DashboardLayoutConfig(
widgets: [
DashboardWidgetConfig(id: 'clock', type: 'clock', position: 0),
DashboardWidgetConfig(id: 'weather', type: 'weather', position: 1),
DashboardWidgetConfig(id: 'calendar', type: 'calendar', position: 2),
DashboardWidgetConfig(
id: 'ha_entities', type: 'ha_entities', position: 3),
],
);
factory DashboardLayoutConfig.fromJson(Map<String, dynamic> json) =>
DashboardLayoutConfig(
widgets: (json['widgets'] as List<dynamic>)
.map((e) => DashboardWidgetConfig.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> toJson() => {
'widgets': widgets.map((e) => e.toJson()).toList(),
};
}
@@ -0,0 +1,233 @@
import 'dart:convert';
import 'commands.dart';
import 'dashboard_config.dart';
import 'state.dart';
sealed class MirrorMessage {
String get type;
Map<String, dynamic> toJson();
String encode() => jsonEncode(toJson());
static MirrorMessage decode(String raw) {
final json = jsonDecode(raw) as Map<String, dynamic>;
return MirrorMessage.fromJson(json);
}
static MirrorMessage fromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
return switch (type) {
'state_sync' => StateSync.fromJson(json),
'state_patch' => StatePatch.fromJson(json),
'navigate' => NavigateCommand.fromJson(json),
'playback' => PlaybackCommand.fromJson(json),
'workout' => WorkoutCommand.fromJson(json),
'program' => ProgramCommand.fromJson(json),
'media' => MediaCommand.fromJson(json),
'event' => EventMessage.fromJson(json),
'register' => RegisterMessage.fromJson(json),
'dashboard_config_request' => DashboardConfigRequest.fromJson(json),
'dashboard_config_update' => DashboardConfigUpdate.fromJson(json),
'dashboard_config_response' => DashboardConfigResponse.fromJson(json),
_ => throw FormatException('Unknown message type: $type'),
};
}
}
// --- Server → Client messages ---
class StateSync extends MirrorMessage {
@override
String get type => 'state_sync';
final DisplayState state;
StateSync({required this.state});
static StateSync fromJson(Map<String, dynamic> json) =>
StateSync(state: DisplayState.fromJson(json['state'] as Map<String, dynamic>));
@override
Map<String, dynamic> toJson() => {'type': type, 'state': state.toJson()};
}
class StatePatch extends MirrorMessage {
@override
String get type => 'state_patch';
final Map<String, dynamic> patch;
StatePatch({required this.patch});
static StatePatch fromJson(Map<String, dynamic> json) =>
StatePatch(patch: json['patch'] as Map<String, dynamic>);
@override
Map<String, dynamic> toJson() => {'type': type, 'patch': patch};
}
class EventMessage extends MirrorMessage {
@override
String get type => 'event';
final String event;
final Map<String, dynamic>? data;
EventMessage({required this.event, this.data});
static EventMessage fromJson(Map<String, dynamic> json) => EventMessage(
event: json['event'] as String,
data: json['data'] as Map<String, dynamic>?,
);
@override
Map<String, dynamic> toJson() => {'type': type, 'event': event, if (data != null) 'data': data};
}
// --- Client → Server messages ---
class RegisterMessage extends MirrorMessage {
@override
String get type => 'register';
final String role;
RegisterMessage({required this.role});
static RegisterMessage fromJson(Map<String, dynamic> json) =>
RegisterMessage(role: json['role'] as String);
@override
Map<String, dynamic> toJson() => {'type': type, 'role': role};
}
class NavigateCommand extends MirrorMessage {
@override
String get type => 'navigate';
final ScreenType target;
NavigateCommand({required this.target});
static NavigateCommand fromJson(Map<String, dynamic> json) =>
NavigateCommand(target: ScreenType.fromJson(json['target'] as String));
@override
Map<String, dynamic> toJson() => {'type': type, 'target': target.toJson()};
}
class PlaybackCommand extends MirrorMessage {
@override
String get type => 'playback';
final PlaybackAction action;
final dynamic value;
PlaybackCommand({required this.action, this.value});
static PlaybackCommand fromJson(Map<String, dynamic> json) => PlaybackCommand(
action: PlaybackAction.fromJson(json['action'] as String),
value: json['value'],
);
@override
Map<String, dynamic> toJson() =>
{'type': type, 'action': action.toJson(), if (value != null) 'value': value};
}
class WorkoutCommand extends MirrorMessage {
@override
String get type => 'workout';
final WorkoutAction action;
final Map<String, dynamic>? payload;
WorkoutCommand({required this.action, this.payload});
static WorkoutCommand fromJson(Map<String, dynamic> json) => WorkoutCommand(
action: WorkoutAction.fromJson(json['action'] as String),
payload: json['payload'] as Map<String, dynamic>?,
);
@override
Map<String, dynamic> toJson() =>
{'type': type, 'action': action.toJson(), if (payload != null) 'payload': payload};
}
class ProgramCommand extends MirrorMessage {
@override
String get type => 'program';
final ProgramAction action;
final Map<String, dynamic>? payload;
ProgramCommand({required this.action, this.payload});
static ProgramCommand fromJson(Map<String, dynamic> json) => ProgramCommand(
action: ProgramAction.fromJson(json['action'] as String),
payload: json['payload'] as Map<String, dynamic>?,
);
@override
Map<String, dynamic> toJson() =>
{'type': type, 'action': action.toJson(), if (payload != null) 'payload': payload};
}
class MediaCommand extends MirrorMessage {
@override
String get type => 'media';
final MediaAction action;
final Map<String, dynamic>? payload;
MediaCommand({required this.action, this.payload});
static MediaCommand fromJson(Map<String, dynamic> json) => MediaCommand(
action: MediaAction.fromJson(json['action'] as String),
payload: json['payload'] as Map<String, dynamic>?,
);
@override
Map<String, dynamic> toJson() =>
{'type': type, 'action': action.toJson(), if (payload != null) 'payload': payload};
}
// --- Dashboard config messages ---
class DashboardConfigRequest extends MirrorMessage {
@override
String get type => 'dashboard_config_request';
DashboardConfigRequest();
static DashboardConfigRequest fromJson(Map<String, dynamic> json) =>
DashboardConfigRequest();
@override
Map<String, dynamic> toJson() => {'type': type};
}
class DashboardConfigUpdate extends MirrorMessage {
@override
String get type => 'dashboard_config_update';
final DashboardLayoutConfig layout;
DashboardConfigUpdate({required this.layout});
static DashboardConfigUpdate fromJson(Map<String, dynamic> json) =>
DashboardConfigUpdate(
layout:
DashboardLayoutConfig.fromJson(json['layout'] as Map<String, dynamic>),
);
@override
Map<String, dynamic> toJson() => {'type': type, 'layout': layout.toJson()};
}
class DashboardConfigResponse extends MirrorMessage {
@override
String get type => 'dashboard_config_response';
final DashboardLayoutConfig layout;
DashboardConfigResponse({required this.layout});
static DashboardConfigResponse fromJson(Map<String, dynamic> json) =>
DashboardConfigResponse(
layout:
DashboardLayoutConfig.fromJson(json['layout'] as Map<String, dynamic>),
);
@override
Map<String, dynamic> toJson() => {'type': type, 'layout': layout.toJson()};
}
+554
View File
@@ -0,0 +1,554 @@
import 'commands.dart';
class DisplayState {
final ScreenType activeScreen;
final PlaybackState? playback;
final WorkoutSessionState? workout;
final ProgramSessionState? program;
final SpotifyState? spotify;
final DashboardState dashboard;
const DisplayState({
required this.activeScreen,
this.playback,
this.workout,
this.program,
this.spotify,
required this.dashboard,
});
DisplayState copyWith({
ScreenType? activeScreen,
PlaybackState? playback,
bool clearPlayback = false,
WorkoutSessionState? workout,
bool clearWorkout = false,
ProgramSessionState? program,
bool clearProgram = false,
SpotifyState? spotify,
bool clearSpotify = false,
DashboardState? dashboard,
}) =>
DisplayState(
activeScreen: activeScreen ?? this.activeScreen,
playback: clearPlayback ? null : (playback ?? this.playback),
workout: clearWorkout ? null : (workout ?? this.workout),
program: clearProgram ? null : (program ?? this.program),
spotify: clearSpotify ? null : (spotify ?? this.spotify),
dashboard: dashboard ?? this.dashboard,
);
factory DisplayState.initial() => DisplayState(
activeScreen: ScreenType.dashboard,
dashboard: DashboardState(),
);
factory DisplayState.fromJson(Map<String, dynamic> json) => DisplayState(
activeScreen: ScreenType.fromJson(json['activeScreen'] as String),
playback: json['playback'] != null
? PlaybackState.fromJson(json['playback'] as Map<String, dynamic>)
: null,
workout: json['workout'] != null
? WorkoutSessionState.fromJson(json['workout'] as Map<String, dynamic>)
: null,
program: json['program'] != null
? ProgramSessionState.fromJson(json['program'] as Map<String, dynamic>)
: null,
spotify: json['spotify'] != null
? SpotifyState.fromJson(json['spotify'] as Map<String, dynamic>)
: null,
dashboard: json['dashboard'] != null
? DashboardState.fromJson(json['dashboard'] as Map<String, dynamic>)
: DashboardState(),
);
Map<String, dynamic> toJson() => {
'activeScreen': activeScreen.toJson(),
if (playback != null) 'playback': playback!.toJson(),
if (workout != null) 'workout': workout!.toJson(),
if (program != null) 'program': program!.toJson(),
if (spotify != null) 'spotify': spotify!.toJson(),
'dashboard': dashboard.toJson(),
};
}
class PlaybackState {
final String mediaId;
final String title;
final MediaSource source;
final Duration position;
final Duration duration;
final bool isPlaying;
final double volume;
const PlaybackState({
required this.mediaId,
required this.title,
required this.source,
required this.position,
required this.duration,
required this.isPlaying,
this.volume = 1.0,
});
PlaybackState copyWith({
String? mediaId,
String? title,
MediaSource? source,
Duration? position,
Duration? duration,
bool? isPlaying,
double? volume,
}) =>
PlaybackState(
mediaId: mediaId ?? this.mediaId,
title: title ?? this.title,
source: source ?? this.source,
position: position ?? this.position,
duration: duration ?? this.duration,
isPlaying: isPlaying ?? this.isPlaying,
volume: volume ?? this.volume,
);
factory PlaybackState.fromJson(Map<String, dynamic> json) => PlaybackState(
mediaId: json['mediaId'] as String,
title: json['title'] as String,
source: MediaSource.fromJson(json['source'] as String),
position: Duration(milliseconds: json['positionMs'] as int),
duration: Duration(milliseconds: json['durationMs'] as int),
isPlaying: json['isPlaying'] as bool,
volume: (json['volume'] as num?)?.toDouble() ?? 1.0,
);
Map<String, dynamic> toJson() => {
'mediaId': mediaId,
'title': title,
'source': source.toJson(),
'positionMs': position.inMilliseconds,
'durationMs': duration.inMilliseconds,
'isPlaying': isPlaying,
'volume': volume,
};
}
class WorkoutSessionState {
final WorkoutPhase phase;
final List<QueueItemState> queue;
final int currentIndex;
final int restRemaining;
final String? routineName;
final String? currentPath;
final WorkoutFilter filter;
final List<VideoState> videos;
final List<String> folders;
final WorkoutStats? stats;
const WorkoutSessionState({
required this.phase,
this.queue = const [],
this.currentIndex = 0,
this.restRemaining = 0,
this.routineName,
this.currentPath,
this.filter = WorkoutFilter.all,
this.videos = const [],
this.folders = const [],
this.stats,
});
WorkoutSessionState copyWith({
WorkoutPhase? phase,
List<QueueItemState>? queue,
int? currentIndex,
int? restRemaining,
String? routineName,
String? currentPath,
bool clearCurrentPath = false,
WorkoutFilter? filter,
List<VideoState>? videos,
List<String>? folders,
WorkoutStats? stats,
}) =>
WorkoutSessionState(
phase: phase ?? this.phase,
queue: queue ?? this.queue,
currentIndex: currentIndex ?? this.currentIndex,
restRemaining: restRemaining ?? this.restRemaining,
routineName: routineName ?? this.routineName,
currentPath: clearCurrentPath ? null : (currentPath ?? this.currentPath),
filter: filter ?? this.filter,
videos: videos ?? this.videos,
folders: folders ?? this.folders,
stats: stats ?? this.stats,
);
factory WorkoutSessionState.fromJson(Map<String, dynamic> json) => WorkoutSessionState(
phase: WorkoutPhase.fromJson(json['phase'] as String),
queue: (json['queue'] as List<dynamic>?)
?.map((e) => QueueItemState.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
currentIndex: json['currentIndex'] as int? ?? 0,
restRemaining: json['restRemaining'] as int? ?? 0,
routineName: json['routineName'] as String?,
currentPath: json['currentPath'] as String?,
filter: json['filter'] != null
? WorkoutFilter.fromJson(json['filter'] as String)
: WorkoutFilter.all,
videos: (json['videos'] as List<dynamic>?)
?.map((e) => VideoState.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
folders:
(json['folders'] as List<dynamic>?)?.map((e) => e as String).toList() ?? [],
stats: json['stats'] != null
? WorkoutStats.fromJson(json['stats'] as Map<String, dynamic>)
: null,
);
Map<String, dynamic> toJson() => {
'phase': phase.toJson(),
'queue': queue.map((e) => e.toJson()).toList(),
'currentIndex': currentIndex,
'restRemaining': restRemaining,
if (routineName != null) 'routineName': routineName,
if (currentPath != null) 'currentPath': currentPath,
'filter': filter.toJson(),
'videos': videos.map((e) => e.toJson()).toList(),
'folders': folders,
if (stats != null) 'stats': stats!.toJson(),
};
}
class QueueItemState {
final String videoId;
final String title;
final String path;
final int duration;
final int trimStart;
final int trimEnd;
final int restSeconds;
const QueueItemState({
required this.videoId,
required this.title,
required this.path,
this.duration = 0,
this.trimStart = 0,
this.trimEnd = 0,
this.restSeconds = 0,
});
factory QueueItemState.fromJson(Map<String, dynamic> json) => QueueItemState(
videoId: json['videoId'] as String,
title: json['title'] as String,
path: json['path'] as String,
duration: json['duration'] as int? ?? 0,
trimStart: json['trimStart'] as int? ?? 0,
trimEnd: json['trimEnd'] as int? ?? 0,
restSeconds: json['restSeconds'] as int? ?? 0,
);
Map<String, dynamic> toJson() => {
'videoId': videoId,
'title': title,
'path': path,
'duration': duration,
'trimStart': trimStart,
'trimEnd': trimEnd,
'restSeconds': restSeconds,
};
}
class VideoState {
final String id;
final String title;
final String path;
final String? folder;
final String? thumbnailPath;
final int duration;
final int trimStart;
final int trimEnd;
final int lastPosition;
final int percentCompleted;
final int restSeconds;
const VideoState({
required this.id,
required this.title,
required this.path,
this.folder,
this.thumbnailPath,
this.duration = 0,
this.trimStart = 0,
this.trimEnd = 0,
this.lastPosition = 0,
this.percentCompleted = 0,
this.restSeconds = 0,
});
factory VideoState.fromJson(Map<String, dynamic> json) => VideoState(
id: json['id'] as String,
title: json['title'] as String,
path: json['path'] as String,
folder: json['folder'] as String?,
thumbnailPath: json['thumbnailPath'] as String?,
duration: json['duration'] as int? ?? 0,
trimStart: json['trimStart'] as int? ?? 0,
trimEnd: json['trimEnd'] as int? ?? 0,
lastPosition: json['lastPosition'] as int? ?? 0,
percentCompleted: json['percentCompleted'] as int? ?? 0,
restSeconds: json['restSeconds'] as int? ?? 0,
);
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'path': path,
if (folder != null) 'folder': folder,
if (thumbnailPath != null) 'thumbnailPath': thumbnailPath,
'duration': duration,
'trimStart': trimStart,
'trimEnd': trimEnd,
'lastPosition': lastPosition,
'percentCompleted': percentCompleted,
'restSeconds': restSeconds,
};
}
class WorkoutStats {
final int total;
final int completed;
final int inProgress;
const WorkoutStats({
required this.total,
required this.completed,
required this.inProgress,
});
factory WorkoutStats.fromJson(Map<String, dynamic> json) => WorkoutStats(
total: json['total'] as int,
completed: json['completed'] as int,
inProgress: json['inProgress'] as int,
);
Map<String, dynamic> toJson() => {
'total': total,
'completed': completed,
'inProgress': inProgress,
};
}
class DashboardState {
final List<String> widgets;
final WeatherInfo? weather;
final List<CalendarEvent> calendarEvents;
final List<Map<String, dynamic>> haEntities;
DashboardState({
this.widgets = const ['clock', 'weather', 'calendar'],
this.weather,
this.calendarEvents = const [],
this.haEntities = const [],
});
factory DashboardState.fromJson(Map<String, dynamic> json) => DashboardState(
widgets: (json['widgets'] as List<dynamic>?)?.map((e) => e as String).toList() ??
['clock', 'weather', 'calendar'],
weather: json['weather'] != null
? WeatherInfo.fromJson(json['weather'] as Map<String, dynamic>)
: null,
calendarEvents: (json['calendarEvents'] as List<dynamic>?)
?.map((e) => CalendarEvent.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
haEntities: (json['haEntities'] as List<dynamic>?)
?.map((e) => e as Map<String, dynamic>)
.toList() ??
[],
);
Map<String, dynamic> toJson() => {
'widgets': widgets,
if (weather != null) 'weather': weather!.toJson(),
'calendarEvents': calendarEvents.map((e) => e.toJson()).toList(),
'haEntities': haEntities,
};
}
class WeatherInfo {
final double temp;
final String condition;
final int? humidity;
const WeatherInfo({
required this.temp,
required this.condition,
this.humidity,
});
factory WeatherInfo.fromJson(Map<String, dynamic> json) => WeatherInfo(
temp: (json['temp'] as num).toDouble(),
condition: json['condition'] as String,
humidity: json['humidity'] as int?,
);
Map<String, dynamic> toJson() => {
'temp': temp,
'condition': condition,
if (humidity != null) 'humidity': humidity,
};
}
class CalendarEvent {
final String summary;
final String startTime;
final String endTime;
const CalendarEvent({
required this.summary,
required this.startTime,
required this.endTime,
});
factory CalendarEvent.fromJson(Map<String, dynamic> json) => CalendarEvent(
summary: json['summary'] as String,
startTime: json['startTime'] as String,
endTime: json['endTime'] as String,
);
Map<String, dynamic> toJson() => {
'summary': summary,
'startTime': startTime,
'endTime': endTime,
};
}
class ProgramSessionState {
final String programId;
final String programName;
final String workoutName;
final List<String> exercises;
final int currentExerciseIndex;
final String phase; // playing, resting, instruction, completed
final int timerRemaining;
final int currentSet;
final int totalSets;
const ProgramSessionState({
required this.programId,
required this.programName,
required this.workoutName,
this.exercises = const [],
this.currentExerciseIndex = 0,
this.phase = 'playing',
this.timerRemaining = 0,
this.currentSet = 1,
this.totalSets = 1,
});
ProgramSessionState copyWith({
String? programId,
String? programName,
String? workoutName,
List<String>? exercises,
int? currentExerciseIndex,
String? phase,
int? timerRemaining,
int? currentSet,
int? totalSets,
}) =>
ProgramSessionState(
programId: programId ?? this.programId,
programName: programName ?? this.programName,
workoutName: workoutName ?? this.workoutName,
exercises: exercises ?? this.exercises,
currentExerciseIndex: currentExerciseIndex ?? this.currentExerciseIndex,
phase: phase ?? this.phase,
timerRemaining: timerRemaining ?? this.timerRemaining,
currentSet: currentSet ?? this.currentSet,
totalSets: totalSets ?? this.totalSets,
);
factory ProgramSessionState.fromJson(Map<String, dynamic> json) =>
ProgramSessionState(
programId: json['programId'] as String,
programName: json['programName'] as String,
workoutName: json['workoutName'] as String,
exercises: (json['exercises'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
currentExerciseIndex: json['currentExerciseIndex'] as int? ?? 0,
phase: json['phase'] as String? ?? 'playing',
timerRemaining: json['timerRemaining'] as int? ?? 0,
currentSet: json['currentSet'] as int? ?? 1,
totalSets: json['totalSets'] as int? ?? 1,
);
Map<String, dynamic> toJson() => {
'programId': programId,
'programName': programName,
'workoutName': workoutName,
'exercises': exercises,
'currentExerciseIndex': currentExerciseIndex,
'phase': phase,
'timerRemaining': timerRemaining,
'currentSet': currentSet,
'totalSets': totalSets,
};
}
class SpotifyState {
final bool isPlaying;
final String? trackName;
final String? artistName;
final String? albumImageUrl;
final int progressMs;
final int durationMs;
const SpotifyState({
this.isPlaying = false,
this.trackName,
this.artistName,
this.albumImageUrl,
this.progressMs = 0,
this.durationMs = 0,
});
SpotifyState copyWith({
bool? isPlaying,
String? trackName,
String? artistName,
String? albumImageUrl,
int? progressMs,
int? durationMs,
}) =>
SpotifyState(
isPlaying: isPlaying ?? this.isPlaying,
trackName: trackName ?? this.trackName,
artistName: artistName ?? this.artistName,
albumImageUrl: albumImageUrl ?? this.albumImageUrl,
progressMs: progressMs ?? this.progressMs,
durationMs: durationMs ?? this.durationMs,
);
factory SpotifyState.fromJson(Map<String, dynamic> json) => SpotifyState(
isPlaying: json['isPlaying'] as bool? ?? false,
trackName: json['trackName'] as String?,
artistName: json['artistName'] as String?,
albumImageUrl: json['albumImageUrl'] as String?,
progressMs: json['progressMs'] as int? ?? 0,
durationMs: json['durationMs'] as int? ?? 0,
);
Map<String, dynamic> toJson() => {
'isPlaying': isPlaying,
if (trackName != null) 'trackName': trackName,
if (artistName != null) 'artistName': artistName,
if (albumImageUrl != null) 'albumImageUrl': albumImageUrl,
'progressMs': progressMs,
'durationMs': durationMs,
};
}
+11
View File
@@ -0,0 +1,11 @@
name: mirror_protocol
description: WebSocket message types and serialization for Mirror OS communication
version: 0.1.0
publish_to: none
resolution: workspace
environment:
sdk: ^3.5.0
dependencies:
uuid: ^4.5.1