355 lines
8.7 KiB
Dart
355 lines
8.7 KiB
Dart
|
|
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<String, dynamic> toJson() => {
|
||
|
|
'id': id,
|
||
|
|
'name': name,
|
||
|
|
'url': url,
|
||
|
|
'enabled': enabled,
|
||
|
|
};
|
||
|
|
|
||
|
|
factory DanmakuSource.fromJson(Map<String, dynamic> 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<DanmakuMatchCandidate> matches;
|
||
|
|
final int selectedIndex;
|
||
|
|
final DanmakuCommentStatus commentStatus;
|
||
|
|
final List<DanmakuComment> comments;
|
||
|
|
|
||
|
|
const DanmakuSessionMatched({
|
||
|
|
required this.matches,
|
||
|
|
required this.selectedIndex,
|
||
|
|
required this.commentStatus,
|
||
|
|
this.comments = const [],
|
||
|
|
});
|
||
|
|
|
||
|
|
DanmakuSessionMatched copyWith({
|
||
|
|
List<DanmakuMatchCandidate>? matches,
|
||
|
|
int? selectedIndex,
|
||
|
|
DanmakuCommentStatus? commentStatus,
|
||
|
|
List<DanmakuComment>? 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<String, dynamic> 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<String, dynamic> 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,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|