import 'package:flutter/painting.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'danmaku.freezed.dart'; part 'danmaku.g.dart'; class DanmakuPanelSettings { final bool enabled; final double opacity; final double area; final double fontSize; final double speed; final double offset; final bool showTop; final bool showScroll; final bool showBottom; final bool colored; final double strokeWidth; final bool bold; final String? fontFamily; final double lineHeight; final bool fixedToScroll; final int maxRows; final bool heatmapEnabled; const DanmakuPanelSettings({ this.enabled = true, this.opacity = 1.0, this.area = 0.85, this.fontSize = 20, this.speed = 1.0, this.offset = 0.0, this.showTop = true, this.showScroll = true, this.showBottom = false, this.colored = true, this.strokeWidth = 1.5, this.bold = false, this.fontFamily, this.lineHeight = 1.6, this.fixedToScroll = false, this.maxRows = 0, this.heatmapEnabled = true, }); DanmakuPanelSettings copyWith({ bool? enabled, double? opacity, double? area, double? fontSize, double? speed, double? offset, bool? showTop, bool? showScroll, bool? showBottom, bool? colored, double? strokeWidth, bool? bold, Object? fontFamily = _sentinel, double? lineHeight, bool? fixedToScroll, int? maxRows, bool? heatmapEnabled, }) => DanmakuPanelSettings( enabled: enabled ?? this.enabled, opacity: opacity ?? this.opacity, area: area ?? this.area, fontSize: fontSize ?? this.fontSize, speed: speed ?? this.speed, offset: offset ?? this.offset, showTop: showTop ?? this.showTop, showScroll: showScroll ?? this.showScroll, showBottom: showBottom ?? this.showBottom, colored: colored ?? this.colored, strokeWidth: strokeWidth ?? this.strokeWidth, bold: bold ?? this.bold, fontFamily: fontFamily == _sentinel ? this.fontFamily : fontFamily as String?, lineHeight: lineHeight ?? this.lineHeight, fixedToScroll: fixedToScroll ?? this.fixedToScroll, maxRows: maxRows ?? this.maxRows, heatmapEnabled: heatmapEnabled ?? this.heatmapEnabled, ); @override bool operator ==(Object other) { return identical(this, other) || other is DanmakuPanelSettings && enabled == other.enabled && opacity == other.opacity && area == other.area && fontSize == other.fontSize && speed == other.speed && offset == other.offset && showTop == other.showTop && showScroll == other.showScroll && showBottom == other.showBottom && colored == other.colored && strokeWidth == other.strokeWidth && bold == other.bold && fontFamily == other.fontFamily && lineHeight == other.lineHeight && fixedToScroll == other.fixedToScroll && maxRows == other.maxRows && heatmapEnabled == other.heatmapEnabled; } @override int get hashCode => Object.hash( enabled, opacity, area, fontSize, speed, offset, showTop, showScroll, showBottom, colored, strokeWidth, bold, fontFamily, lineHeight, fixedToScroll, maxRows, heatmapEnabled, ); } const Object _sentinel = Object(); class DanmakuSource { final String id; final String name; final String url; final bool enabled; const DanmakuSource({ required this.id, required this.name, required this.url, this.enabled = true, }); DanmakuSource copyWith({ String? id, String? name, String? url, bool? enabled, }) => DanmakuSource( id: id ?? this.id, name: name ?? this.name, url: url ?? this.url, enabled: enabled ?? this.enabled, ); String get displayName { final trimmed = name.trim(); if (trimmed.isNotEmpty) return trimmed; return url; } Map toJson() => { 'id': id, 'name': name, 'url': url, 'enabled': enabled, }; factory DanmakuSource.fromJson(Map json) { final url = (json['url'] ?? '').toString().trim(); return DanmakuSource( id: (json['id'] ?? url).toString(), name: (json['name'] ?? '').toString(), url: url, enabled: json['enabled'] is bool ? json['enabled'] as bool : true, ); } @override bool operator ==(Object other) { return identical(this, other) || other is DanmakuSource && id == other.id && name == other.name && url == other.url && enabled == other.enabled; } @override int get hashCode => Object.hash(id, name, url, enabled); } enum DanmakuCommentStatus { loading, ready, failed } sealed class DanmakuSessionState { const DanmakuSessionState(); } class DanmakuSessionIdle extends DanmakuSessionState { const DanmakuSessionIdle(); } class DanmakuSessionMatching extends DanmakuSessionState { const DanmakuSessionMatching(); } class DanmakuSessionMatched extends DanmakuSessionState { final List matches; final int selectedIndex; final DanmakuCommentStatus commentStatus; final List comments; const DanmakuSessionMatched({ required this.matches, required this.selectedIndex, required this.commentStatus, this.comments = const [], }); DanmakuSessionMatched copyWith({ List? matches, int? selectedIndex, DanmakuCommentStatus? commentStatus, List? comments, }) => DanmakuSessionMatched( matches: matches ?? this.matches, selectedIndex: selectedIndex ?? this.selectedIndex, commentStatus: commentStatus ?? this.commentStatus, comments: comments ?? this.comments, ); } class DanmakuSessionEmpty extends DanmakuSessionState { const DanmakuSessionEmpty(); } class DanmakuSessionFailed extends DanmakuSessionState { final String message; const DanmakuSessionFailed(this.message); } @freezed abstract class DanmakuMatchContext with _$DanmakuMatchContext { const factory DanmakuMatchContext({ required String itemId, required String name, required String? type, required String? seriesName, required int? season, required int? episode, required int durationSec, }) = _DanmakuMatchContext; } @freezed abstract class DanmakuMatch with _$DanmakuMatch { const factory DanmakuMatch({ @JsonKey(fromJson: _intRequired) required int episodeId, @JsonKey(fromJson: _intOrZero) @Default(0) int animeId, @Default('') String animeTitle, @Default('') String episodeTitle, }) = _DanmakuMatch; factory DanmakuMatch.fromJson(Map json) => _$DanmakuMatchFromJson(json); } class DanmakuMatchCandidate { final DanmakuSource source; final DanmakuMatch match; const DanmakuMatchCandidate({required this.source, required this.match}); int get episodeId => match.episodeId; int get animeId => match.animeId; String get animeTitle => match.animeTitle; String get episodeTitle => match.episodeTitle; @override bool operator ==(Object other) { return identical(this, other) || other is DanmakuMatchCandidate && source == other.source && match == other.match; } @override int get hashCode => Object.hash(source, match); } @freezed abstract class DanmakuBangumiEpisode with _$DanmakuBangumiEpisode { const factory DanmakuBangumiEpisode({ @JsonKey(fromJson: _intRequired) required int episodeId, @Default('') String episodeTitle, @JsonKey(fromJson: _stringFromAny) @Default('') String episodeNumber, }) = _DanmakuBangumiEpisode; factory DanmakuBangumiEpisode.fromJson(Map json) => _$DanmakuBangumiEpisodeFromJson(json); } int _intRequired(Object? value) => (value as num).toInt(); int _intOrZero(Object? value) => (value as num?)?.toInt() ?? 0; String _stringFromAny(Object? value) => value?.toString() ?? ''; class DanmakuComment { final int cid; final double time; final int type; final Color color; final String text; int lane; DanmakuComment({ required this.cid, required this.time, required this.type, required this.color, required this.text, this.lane = 0, }); factory DanmakuComment.fromRaw(int cid, String p, String m) { final parts = p.split(','); final time = parts.isNotEmpty ? (double.tryParse(parts[0]) ?? 0.0) : 0.0; final type = parts.length > 1 ? (int.tryParse(parts[1]) ?? 1) : 1; final colorInt = parts.length > 2 ? (int.tryParse(parts[2]) ?? 0xFFFFFF) : 0xFFFFFF; return DanmakuComment( cid: cid, time: time, type: type, color: Color(0xFF000000 | (colorInt & 0xFFFFFF)), text: m, ); } }