Light theme default + yt-dlp search fallback

This commit is contained in:
iliasmouhcine
2026-08-22 13:02:32 +01:00
commit 7704cae4a0
282 changed files with 52324 additions and 0 deletions
+39
View File
@@ -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;
}
+25
View File
@@ -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;
}
+17
View File
@@ -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;
}
+102
View File
@@ -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;
}
+13
View File
@@ -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;
}
+113
View File
@@ -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,
);
}
}