Light theme default + yt-dlp search fallback
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
class ChannelDetails {
|
||||
const ChannelDetails({
|
||||
required this.channelId,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.thumbnailUrl,
|
||||
this.publishedAt,
|
||||
this.joinDateLabel,
|
||||
this.viewCount,
|
||||
this.subscriberCount,
|
||||
this.hiddenSubscriberCount = false,
|
||||
this.videoCount,
|
||||
this.country,
|
||||
});
|
||||
|
||||
final String channelId;
|
||||
final String title;
|
||||
final String description;
|
||||
final String thumbnailUrl;
|
||||
|
||||
/// Channel creation date from the official YouTube Data API.
|
||||
final DateTime? publishedAt;
|
||||
|
||||
/// Human-readable join date returned by the public YouTube About page.
|
||||
/// Used when no Google OAuth session is active.
|
||||
final String? joinDateLabel;
|
||||
|
||||
/// Public total views for the channel.
|
||||
final int? viewCount;
|
||||
|
||||
/// Public subscriber count. YouTube may hide it for some channels.
|
||||
final int? subscriberCount;
|
||||
final bool hiddenSubscriberCount;
|
||||
|
||||
/// Number of public videos reported by YouTube.
|
||||
final int? videoCount;
|
||||
|
||||
final String? country;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'subscription_channel.dart';
|
||||
|
||||
class ChannelRankItem {
|
||||
const ChannelRankItem({
|
||||
required this.channel,
|
||||
required this.personalViewCount,
|
||||
required this.distinctVideoCount,
|
||||
required this.topVideoPersonalViews,
|
||||
this.topVideoTitle,
|
||||
});
|
||||
|
||||
final SubscriptionChannel channel;
|
||||
|
||||
/// Number of times the user actually opened videos from this channel in
|
||||
/// YouTube Personal. This is the primary ranking metric.
|
||||
final int personalViewCount;
|
||||
|
||||
/// Distinct history videos currently known for this channel.
|
||||
final int distinctVideoCount;
|
||||
|
||||
/// Personal repeat count for the user's most watched known video.
|
||||
final int topVideoPersonalViews;
|
||||
|
||||
final String? topVideoTitle;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
class YoutubePlaybackOption {
|
||||
const YoutubePlaybackOption({
|
||||
required this.height,
|
||||
required this.label,
|
||||
required this.videoUrl,
|
||||
this.audioUrl,
|
||||
this.fps,
|
||||
});
|
||||
|
||||
final int height;
|
||||
final String label;
|
||||
final String videoUrl;
|
||||
final String? audioUrl;
|
||||
final num? fps;
|
||||
|
||||
bool get isAdaptive => audioUrl != null;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'video_item.dart';
|
||||
|
||||
enum RadarSessionMode {
|
||||
normal,
|
||||
discovery,
|
||||
learn,
|
||||
relax,
|
||||
short,
|
||||
long,
|
||||
}
|
||||
|
||||
extension RadarSessionModeLabel on RadarSessionMode {
|
||||
String get label {
|
||||
switch (this) {
|
||||
case RadarSessionMode.normal:
|
||||
return 'Comme d’habitude';
|
||||
case RadarSessionMode.discovery:
|
||||
return 'Je veux du nouveau';
|
||||
case RadarSessionMode.learn:
|
||||
return 'Apprendre';
|
||||
case RadarSessionMode.relax:
|
||||
return 'Détente';
|
||||
case RadarSessionMode.short:
|
||||
return 'Courtes vidéos';
|
||||
case RadarSessionMode.long:
|
||||
return 'Long format';
|
||||
}
|
||||
}
|
||||
|
||||
String get shortLabel {
|
||||
switch (this) {
|
||||
case RadarSessionMode.normal:
|
||||
return 'Normal';
|
||||
case RadarSessionMode.discovery:
|
||||
return 'Découverte';
|
||||
case RadarSessionMode.learn:
|
||||
return 'Apprendre';
|
||||
case RadarSessionMode.relax:
|
||||
return 'Détente';
|
||||
case RadarSessionMode.short:
|
||||
return 'Court';
|
||||
case RadarSessionMode.long:
|
||||
return 'Long';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RadarLane {
|
||||
forYou,
|
||||
continueTopic,
|
||||
discovery,
|
||||
}
|
||||
|
||||
class RadarRecommendation {
|
||||
const RadarRecommendation({
|
||||
required this.video,
|
||||
required this.score,
|
||||
required this.reasons,
|
||||
required this.breakdown,
|
||||
required this.lane,
|
||||
required this.matchedTerms,
|
||||
required this.isNewAuthor,
|
||||
});
|
||||
|
||||
final VideoItem video;
|
||||
final int score;
|
||||
final List<String> reasons;
|
||||
final Map<String, int> breakdown;
|
||||
final RadarLane lane;
|
||||
final List<String> matchedTerms;
|
||||
final bool isNewAuthor;
|
||||
}
|
||||
|
||||
class RadarFeed {
|
||||
const RadarFeed({
|
||||
required this.mode,
|
||||
required this.generatedAt,
|
||||
required this.profileTopics,
|
||||
required this.primary,
|
||||
required this.continueTopic,
|
||||
required this.discovery,
|
||||
this.categories = const <String, List<RadarRecommendation>>{},
|
||||
});
|
||||
|
||||
final RadarSessionMode mode;
|
||||
final DateTime generatedAt;
|
||||
final List<String> profileTopics;
|
||||
final List<RadarRecommendation> primary;
|
||||
final List<RadarRecommendation> continueTopic;
|
||||
final List<RadarRecommendation> discovery;
|
||||
|
||||
/// Primary local classification of proposed videos.
|
||||
///
|
||||
/// A recommendation belongs to one main category so the category lanes
|
||||
/// stay clean and predictable.
|
||||
final Map<String, List<RadarRecommendation>> categories;
|
||||
|
||||
bool get isEmpty =>
|
||||
primary.isEmpty &&
|
||||
continueTopic.isEmpty &&
|
||||
discovery.isEmpty;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
class SubscriptionChannel {
|
||||
const SubscriptionChannel({
|
||||
required this.channelId,
|
||||
required this.title,
|
||||
required this.thumbnailUrl,
|
||||
this.description = '',
|
||||
});
|
||||
|
||||
final String channelId;
|
||||
final String title;
|
||||
final String thumbnailUrl;
|
||||
final String description;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
class VideoItem {
|
||||
const VideoItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.author,
|
||||
required this.thumbnailUrl,
|
||||
this.duration,
|
||||
this.channelId,
|
||||
this.viewCount,
|
||||
this.likeCount,
|
||||
this.commentCount,
|
||||
this.description = '',
|
||||
this.uploadDate,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final String author;
|
||||
final String thumbnailUrl;
|
||||
final Duration? duration;
|
||||
|
||||
/// YouTube channel ID when it is known.
|
||||
///
|
||||
/// Old history/favorites saved before step 18.3 simply have null here.
|
||||
/// The channel screen can resolve it lazily from the video ID.
|
||||
final String? channelId;
|
||||
|
||||
/// Public YouTube view count at the time the metadata was fetched.
|
||||
final int? viewCount;
|
||||
|
||||
/// Public likes for this video when YouTube exposes the statistic.
|
||||
final int? likeCount;
|
||||
|
||||
/// Public comments for this video when YouTube exposes the statistic.
|
||||
final int? commentCount;
|
||||
|
||||
/// Public YouTube video description.
|
||||
final String description;
|
||||
|
||||
/// YouTube upload date when available.
|
||||
final DateTime? uploadDate;
|
||||
|
||||
String get durationLabel {
|
||||
final value = duration;
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
final hours = value.inHours;
|
||||
final minutes = value.inMinutes.remainder(60);
|
||||
final seconds = value.inSeconds.remainder(60);
|
||||
|
||||
if (hours > 0) {
|
||||
return '$hours:${minutes.toString().padLeft(2, '0')}:'
|
||||
'${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'author': author,
|
||||
'thumbnailUrl': thumbnailUrl,
|
||||
'durationSeconds': duration?.inSeconds,
|
||||
if (channelId != null) 'channelId': channelId,
|
||||
if (viewCount != null) 'viewCount': viewCount,
|
||||
if (likeCount != null) 'likeCount': likeCount,
|
||||
if (commentCount != null) 'commentCount': commentCount,
|
||||
if (description.isNotEmpty) 'description': description,
|
||||
if (uploadDate != null)
|
||||
'uploadDate': uploadDate!.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory VideoItem.fromJson(Map<String, dynamic> json) {
|
||||
final durationSeconds = json['durationSeconds'];
|
||||
final rawViewCount = json['viewCount'];
|
||||
final rawLikeCount = json['likeCount'];
|
||||
final rawCommentCount = json['commentCount'];
|
||||
final rawUploadDate = json['uploadDate'];
|
||||
|
||||
return VideoItem(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
author: json['author'] as String,
|
||||
thumbnailUrl: json['thumbnailUrl'] as String,
|
||||
duration: durationSeconds is num
|
||||
? Duration(seconds: durationSeconds.toInt())
|
||||
: null,
|
||||
channelId: json['channelId'] is String
|
||||
? json['channelId'] as String
|
||||
: null,
|
||||
viewCount: rawViewCount is num
|
||||
? rawViewCount.toInt()
|
||||
: null,
|
||||
likeCount: rawLikeCount is num
|
||||
? rawLikeCount.toInt()
|
||||
: null,
|
||||
commentCount: rawCommentCount is num
|
||||
? rawCommentCount.toInt()
|
||||
: null,
|
||||
description: json['description'] is String
|
||||
? json['description'] as String
|
||||
: '',
|
||||
uploadDate: rawUploadDate is String
|
||||
? DateTime.tryParse(rawUploadDate)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user