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
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+52
View File
@@ -0,0 +1,52 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.studioweb.youtube_personal"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.studioweb.youtube_personal"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.␍
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.26.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
}
+45
View File
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.studioweb.youtube_personal"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.studioweb.youtube_personal"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,53 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.studioweb.youtube_personal"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.studioweb.youtube_personal"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
dependencies {
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.26.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
}
@@ -0,0 +1,54 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.studioweb.youtube_personal"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.studioweb.youtube_personal"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.26.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
}
@@ -0,0 +1,50 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.studioweb.youtube_personal"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.studioweb.youtube_personal"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.26.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
}
@@ -0,0 +1,50 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.studioweb.youtube_personal"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.studioweb.youtube_personal"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.26.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
}
+7
View File
@@ -0,0 +1,7 @@
# ============================================================
# YouTube Personal - Android Release / R8
# Rhino / NewPipeExtractor contient des references optionnelles
# a java.beans, API absente du runtime Android.
# ============================================================
-dontwarn java.beans.**
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+74
View File
@@ -0,0 +1,74 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="youtube_personal"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:supportsPictureInPicture="true"
android:resizeableActivity="true">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action
android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action
android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action
android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
@@ -0,0 +1,835 @@
package com.studioweb.youtube_personal;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Android HD/4K extractor.
*
* targetHeight:
* - 0 = Auto (meilleure qualité jusqu'à 2160p)
* - 720 / 1080 / 1440 / 2160 = plafond demandé
*
* Jusqu'à 1080p, AVC/H.264 est prioritaire pour la compatibilité.
* Au-dessus de 1080p, VP9 est prioritaire puis AV1, car les flux
* YouTube 1440p/2160p sont généralement fournis avec ces codecs.
*/
public final class HdStreamExtractor {
private static final Pattern RESOLUTION_HEIGHT_PATTERN =
Pattern.compile("(\\d{3,4})p", Pattern.CASE_INSENSITIVE);
private static boolean initialized = false;
private HdStreamExtractor() {
}
private static synchronized void ensureInitialized() {
if (initialized) {
return;
}
NewPipe.init(new NewPipeDownloader());
initialized = true;
}
public static Map<String, Object> getHdInfo(
final String videoId,
final int targetHeight
) throws Exception {
return getHdInfo(videoId, targetHeight, "auto");
}
public static Map<String, Object> getHdInfo(
final String videoId,
final int targetHeight,
final String preferredLanguage
) throws Exception {
ensureInitialized();
final String watchUrl =
"https://www.youtube.com/watch?v=" + videoId;
final StreamInfo info =
StreamInfo.getInfo(watchUrl);
final List<VideoStream> videoOnlyStreams =
info.getVideoOnlyStreams();
final List<AudioStream> audioStreams =
info.getAudioStreams();
final Set<String> uniqueResolutions =
new LinkedHashSet<>();
final Set<Integer> uniqueHeights =
new LinkedHashSet<>();
for (final VideoStream stream : videoOnlyStreams) {
if (stream == null) {
continue;
}
final String resolution = stream.getResolution();
if (resolution != null && !resolution.isBlank()) {
uniqueResolutions.add(resolution);
}
final int resolvedHeight = streamHeight(stream);
if (resolvedHeight > 0) {
uniqueHeights.add(resolvedHeight);
}
}
final int effectiveTarget =
targetHeight <= 0
? 2160
: Math.min(targetHeight, 2160);
final VideoStream selectedVideo =
selectVideo(
videoOnlyStreams,
effectiveTarget
);
final AudioStream selectedAudio =
selectAudio(
audioStreams,
preferredLanguage
);
if (selectedVideo == null) {
throw new IllegalStateException(
"Aucun flux vidéo direct >=720p "
+ "pour cible "
+ effectiveTarget
);
}
if (selectedAudio == null) {
throw new IllegalStateException(
"Aucun flux audio direct exploitable"
);
}
final int selectedHeight = streamHeight(selectedVideo);
final boolean exactTargetMatched =
targetHeight <= 0 || matchesQualityTier(selectedVideo, effectiveTarget);
final Map<String, Object> result =
new LinkedHashMap<>();
result.put(
"videoUrl",
selectedVideo.getContent()
);
result.put(
"audioUrl",
selectedAudio.getContent()
);
result.put(
"width",
selectedVideo.getWidth()
);
result.put(
"height",
selectedHeight
);
result.put(
"fps",
selectedVideo.getFps()
);
result.put(
"videoCodec",
safeString(selectedVideo.getCodec())
);
result.put(
"audioCodec",
safeString(selectedAudio.getCodec())
);
result.put(
"videoResolution",
selectedVideo.getResolution()
);
result.put(
"targetHeight",
targetHeight
);
result.put(
"resolutions",
new ArrayList<>(uniqueResolutions)
);
result.put(
"availableHeights",
new ArrayList<>(uniqueHeights)
);
result.put(
"exactTargetMatched",
exactTargetMatched
);
result.put(
"durationSeconds",
info.getDuration()
);
final Locale selectedAudioLocale =
selectedAudio.getAudioLocale();
final String selectedAudioLanguage =
selectedAudioLocale != null
? safeString(selectedAudioLocale.getLanguage())
: "und";
result.put(
"audioLanguage",
selectedAudioLanguage
);
result.put(
"audioTrackName",
safeNullableString(
selectedAudio.getAudioTrackName()
)
);
result.put(
"preferredAudioMatched",
matchesPreferredLanguage(
selectedAudio,
preferredLanguage
)
);
result.put(
"availableAudioLanguages",
collectAudioLanguages(audioStreams)
);
result.put(
"availableAudioTracks",
collectAudioTracks(audioStreams)
);
result.put(
"originalAudioSelected",
isOriginalAudioTrack(selectedAudio)
);
result.put(
"source",
"NewPipeExtractor 0.26.3 / direct video+audio / original-audio-first / manual-audio-track / preferred-language / aspect-aware-1440-2160 18.7.26"
);
return result;
}
private static VideoStream selectVideo(
final List<VideoStream> streams,
final int targetHeight
) {
if (streams == null || streams.isEmpty()) {
return null;
}
final List<VideoStream> usable =
new ArrayList<>();
for (final VideoStream stream : streams) {
final int height = streamHeight(stream);
if (stream == null
|| !stream.isUrl()
|| !stream.isVideoOnly()
|| stream.getContent() == null
|| stream.getContent().isBlank()
|| qualityTier(stream) < 720
|| qualityTier(stream) > targetHeight) {
continue;
}
usable.add(stream);
}
if (usable.isEmpty()) {
return null;
}
// YouTube peut exposer un palier 1440p/2160p avec une hauteur
// différente sur les vidéos non 16:9 (ex. 2560x1350, 3840x2026).
// On reconnaît donc le palier par les dimensions réelles.
final List<VideoStream> exactTarget =
new ArrayList<>();
for (final VideoStream stream : usable) {
if (matchesQualityTier(stream, targetHeight)) {
exactTarget.add(stream);
}
}
final List<VideoStream> candidates;
if (!exactTarget.isEmpty()) {
candidates = exactTarget;
} else {
final int bestHeight =
usable.stream()
.mapToInt(HdStreamExtractor::streamHeight)
.max()
.orElse(0);
candidates = new ArrayList<>();
for (final VideoStream stream : usable) {
if (streamHeight(stream) == bestHeight) {
candidates.add(stream);
}
}
}
final int selectedHeight =
candidates.stream()
.mapToInt(HdStreamExtractor::streamHeight)
.max()
.orElse(0);
if (selectedHeight <= 1080) {
final VideoStream avc =
bestWithCodec(
candidates,
"avc"
);
if (avc != null) {
return avc;
}
} else {
// 1440p/2160p YouTube sont généralement VP9 ou AV1.
final VideoStream vp9 =
bestWithCodec(
candidates,
"vp9"
);
if (vp9 != null) {
return vp9;
}
final VideoStream av1 =
bestWithCodec(
candidates,
"av1"
);
if (av1 != null) {
return av1;
}
final VideoStream avc =
bestWithCodec(
candidates,
"avc"
);
if (avc != null) {
return avc;
}
}
return candidates.stream()
.max(videoComparator())
.orElse(null);
}
private static int streamHeight(final VideoStream stream) {
if (stream == null) {
return 0;
}
final int nativeHeight = stream.getHeight();
final String resolution = stream.getResolution();
if (resolution != null && !resolution.isBlank()) {
final Matcher matcher =
RESOLUTION_HEIGHT_PATTERN.matcher(resolution);
if (matcher.find()) {
try {
final int parsed =
Integer.parseInt(matcher.group(1));
// Certaines versions/extracteurs peuvent fournir une
// hauteur native incohérente alors que le libellé
// "1440p"/"2160p" est correct. Pour la sélection de
// qualité on privilégie la hauteur explicite du libellé.
if (parsed >= 240) {
return parsed;
}
} catch (NumberFormatException ignored) {
// Fallback vers getHeight().
}
}
}
return nativeHeight;
}
private static int qualityTier(final VideoStream stream) {
if (stream == null) {
return 0;
}
final int width = Math.max(0, stream.getWidth());
final int height = Math.max(0, streamHeight(stream));
final int nativeHeight = Math.max(0, stream.getHeight());
final int longest = Math.max(width, Math.max(height, nativeHeight));
if (longest >= 3800 || height >= 2000) {
return 2160;
}
if (longest >= 2500 || height >= 1300) {
return 1440;
}
if (longest >= 1900 || height >= 1000) {
return 1080;
}
if (longest >= 1200 || height >= 650) {
return 720;
}
return height;
}
private static boolean matchesQualityTier(
final VideoStream stream,
final int targetHeight
) {
return qualityTier(stream) == targetHeight;
}
private static VideoStream bestWithCodec(
final List<VideoStream> streams,
final String codecFamily
) {
return streams.stream()
.filter(stream ->
codecMatches(
stream.getCodec(),
codecFamily
))
.max(videoComparator())
.orElse(null);
}
private static Comparator<VideoStream> videoComparator() {
return Comparator
.comparingInt(VideoStream::getFps)
.thenComparingInt(VideoStream::getBitrate);
}
private static AudioStream selectAudio(
final List<AudioStream> streams,
final String preferredLanguage
) {
if (streams == null || streams.isEmpty()) {
return null;
}
final List<AudioStream> usable =
new ArrayList<>();
for (final AudioStream stream : streams) {
if (stream == null
|| !stream.isUrl()
|| stream.getContent() == null
|| stream.getContent().isBlank()) {
continue;
}
usable.add(stream);
}
if (usable.isEmpty()) {
return null;
}
final String normalizedLanguage =
normalizeLanguageCode(preferredLanguage);
if (!"auto".equals(normalizedLanguage)) {
final List<AudioStream> preferred =
new ArrayList<>();
for (final AudioStream stream : usable) {
if (matchesPreferredLanguage(
stream,
normalizedLanguage
)) {
preferred.add(stream);
}
}
if (!preferred.isEmpty()) {
return selectBestAudio(preferred);
}
}
// 18.7.25 : en mode Auto, la piste originale passe avant les
// doublages automatiques. NewPipeExtractor expose le type de piste
// audio sur les vidéos multilingues récentes de YouTube.
final List<AudioStream> originals =
new ArrayList<>();
for (final AudioStream stream : usable) {
if (isOriginalAudioTrack(stream)) {
originals.add(stream);
}
}
if (!originals.isEmpty()) {
return selectBestAudio(originals);
}
return selectBestAudio(usable);
}
private static boolean isOriginalAudioTrack(
final AudioStream stream
) {
if (stream == null) {
return false;
}
try {
final Object trackType = stream.getAudioTrackType();
if (trackType != null) {
final String typeText = trackType.toString()
.toLowerCase(Locale.ROOT);
if (typeText.contains("original")) {
return true;
}
}
} catch (final Throwable ignored) {
// Compatibilité défensive : le nom de piste reste un fallback.
}
final String trackName = stream.getAudioTrackName();
if (trackName == null || trackName.isBlank()) {
return false;
}
final String name = trackName.toLowerCase(Locale.ROOT);
return name.contains("original")
|| name.contains("الأصلية")
|| name.contains("أصلي")
|| name.contains("اصلي");
}
private static AudioStream selectBestAudio(
final List<AudioStream> streams
) {
final AudioStream aac =
streams.stream()
.filter(stream ->
codecMatches(
stream.getCodec(),
"aac"
))
.max(audioComparator())
.orElse(null);
if (aac != null) {
return aac;
}
return streams.stream()
.max(audioComparator())
.orElse(null);
}
private static boolean matchesPreferredLanguage(
final AudioStream stream,
final String preferredLanguage
) {
final String normalized =
normalizeLanguageCode(preferredLanguage);
if ("auto".equals(normalized)) {
return false;
}
final Locale locale = stream.getAudioLocale();
if (locale != null) {
final String streamLanguage =
normalizeLanguageCode(locale.getLanguage());
if (normalized.equals(streamLanguage)) {
return true;
}
}
final String trackName =
stream.getAudioTrackName();
if (trackName == null || trackName.isBlank()) {
return false;
}
final String name =
trackName.toLowerCase(Locale.ROOT);
switch (normalized) {
case "fr":
return name.contains("français")
|| name.contains("francais")
|| name.contains("french")
|| name.contains("vf");
case "en":
return name.contains("english")
|| name.contains("anglais");
case "ar":
return name.contains("arabic")
|| name.contains("arabe")
|| name.contains("عربي");
case "de":
return name.contains("deutsch")
|| name.contains("german")
|| name.contains("allemand");
case "es":
return name.contains("español")
|| name.contains("espanol")
|| name.contains("spanish")
|| name.contains("espagnol");
default:
return false;
}
}
private static String normalizeLanguageCode(
final String language
) {
if (language == null || language.isBlank()) {
return "auto";
}
String normalized =
language.trim().toLowerCase(Locale.ROOT);
final int hyphen = normalized.indexOf('-');
if (hyphen > 0) {
normalized = normalized.substring(0, hyphen);
}
final int underscore = normalized.indexOf('_');
if (underscore > 0) {
normalized = normalized.substring(0, underscore);
}
return normalized;
}
private static List<Map<String, Object>> collectAudioTracks(
final List<AudioStream> streams
) {
final Map<String, Map<String, Object>> byKey =
new LinkedHashMap<>();
if (streams == null) {
return new ArrayList<>(byKey.values());
}
for (final AudioStream stream : streams) {
if (stream == null
|| !stream.isUrl()
|| stream.getContent() == null
|| stream.getContent().isBlank()) {
continue;
}
final boolean original = isOriginalAudioTrack(stream);
final Locale locale = stream.getAudioLocale();
final String language = locale != null
? normalizeLanguageCode(locale.getLanguage())
: inferLanguageFromTrackName(stream.getAudioTrackName());
final String key;
if (original) {
key = "original";
} else if (!"auto".equals(language)) {
key = language;
} else {
continue;
}
if (byKey.containsKey(key)) {
continue;
}
final Map<String, Object> item = new LinkedHashMap<>();
item.put("key", key);
item.put("language", "auto".equals(language) ? "und" : language);
final String trackName = stream.getAudioTrackName();
if (trackName != null && !trackName.isBlank()) {
item.put("label", trackName);
} else if (original) {
item.put("label", "Original");
} else {
item.put("label", language.toUpperCase(Locale.ROOT));
}
item.put("original", original);
byKey.put(key, item);
}
return new ArrayList<>(byKey.values());
}
private static String inferLanguageFromTrackName(
final String trackName
) {
if (trackName == null || trackName.isBlank()) {
return "auto";
}
final String name = trackName.toLowerCase(Locale.ROOT);
if (name.contains("arabic")
|| name.contains("arabe")
|| name.contains("عربي")) {
return "ar";
}
if (name.contains("english") || name.contains("anglais")) {
return "en";
}
if (name.contains("français")
|| name.contains("francais")
|| name.contains("french")) {
return "fr";
}
if (name.contains("deutsch")
|| name.contains("german")
|| name.contains("allemand")) {
return "de";
}
if (name.contains("español")
|| name.contains("espanol")
|| name.contains("spanish")
|| name.contains("espagnol")) {
return "es";
}
return "auto";
}
private static List<String> collectAudioLanguages(
final List<AudioStream> streams
) {
final Set<String> languages =
new LinkedHashSet<>();
if (streams == null) {
return new ArrayList<>(languages);
}
for (final AudioStream stream : streams) {
if (stream == null) {
continue;
}
final Locale locale = stream.getAudioLocale();
final String trackName = stream.getAudioTrackName();
if (locale != null
&& locale.getLanguage() != null
&& !locale.getLanguage().isBlank()) {
languages.add(locale.getLanguage());
} else if (trackName != null && !trackName.isBlank()) {
languages.add(trackName);
}
}
return new ArrayList<>(languages);
}
private static Comparator<AudioStream> audioComparator() {
return Comparator.comparingInt(
stream -> Math.max(
stream.getBitrate(),
stream.getAverageBitrate()
)
);
}
private static boolean codecMatches(
final String codec,
final String family
) {
if (codec == null) {
return false;
}
final String value =
codec.toLowerCase(Locale.ROOT);
switch (family) {
case "avc":
return value.contains("avc")
|| value.contains("h264")
|| value.contains("h.264");
case "vp9":
return value.contains("vp9")
|| value.contains("vp09");
case "av1":
return value.contains("av01")
|| value.contains("av1");
case "aac":
return value.contains("mp4a")
|| value.contains("aac");
default:
return value.contains(family);
}
}
private static String safeNullableString(final String value) {
return value == null || value.isBlank()
? ""
: value;
}
private static String safeString(final String value) {
return value == null || value.isBlank()
? "inconnu"
: value;
}
}
@@ -0,0 +1,728 @@
package com.studioweb.youtube_personal;
import android.Manifest;
import android.app.PictureInPictureParams;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Rational;
import android.util.Log;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import com.ryanheise.audioservice.AudioServiceActivity;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
/**
* MainActivity Java.
*
* Cette version évite de dépendre de la compilation Kotlin du module app.
* Elle conserve:
* - PiP Android
* - permission notifications
* - détection écran ON/OFF
* - audio_service
* - canal HD natif
* - volume/luminosité par gestes
* - contrôle KEEP_SCREEN_ON pour la mise en veille
*/
public class MainActivity extends AudioServiceActivity {
private static final String PIP_CHANNEL_NAME =
"com.studioweb.youtube_personal/pip";
private static final String HD_CHANNEL_NAME =
"com.studioweb.youtube_personal/hd";
private static final String DEVICE_CHANNEL_NAME =
"com.studioweb.youtube_personal/device";
private static final String CACHE_CHANNEL_NAME =
"com.studioweb.youtube_personal/cache";
private static final int NOTIFICATION_PERMISSION_REQUEST_CODE = 7201;
private MethodChannel pipChannel;
private MethodChannel hdChannel;
private MethodChannel deviceChannel;
private MethodChannel cacheChannel;
private final ExecutorService hdExecutor =
Executors.newSingleThreadExecutor();
private MethodChannel.Result pendingNotificationPermissionResult;
private boolean screenReceiverRegistered = false;
private final Handler screenPolicyHandler =
new Handler(Looper.getMainLooper());
private Runnable pauseSleepRunnable;
private final BroadcastReceiver screenReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(
Context context,
Intent intent
) {
if (intent == null || pipChannel == null) {
return;
}
final String action = intent.getAction();
if (Intent.ACTION_SCREEN_OFF.equals(action)) {
pipChannel.invokeMethod(
"screenInteractiveChanged",
false
);
} else if (Intent.ACTION_SCREEN_ON.equals(action)) {
pipChannel.invokeMethod(
"screenInteractiveChanged",
true
);
}
}
};
@Override
public void configureFlutterEngine(
@NonNull FlutterEngine flutterEngine
) {
super.configureFlutterEngine(flutterEngine);
pipChannel = new MethodChannel(
flutterEngine
.getDartExecutor()
.getBinaryMessenger(),
PIP_CHANNEL_NAME
);
pipChannel.setMethodCallHandler(
(MethodCall call, MethodChannel.Result result) -> {
switch (call.method) {
case "enterPip":
enterPip(result);
break;
case "requestNotificationPermission":
requestNotificationPermission(result);
break;
case "getOrientation":
result.success(getOrientationLabel(getResources().getConfiguration()));
break;
default:
result.notImplemented();
break;
}
}
);
hdChannel = new MethodChannel(
flutterEngine
.getDartExecutor()
.getBinaryMessenger(),
HD_CHANNEL_NAME
);
hdChannel.setMethodCallHandler(
(MethodCall call, MethodChannel.Result result) -> {
if (!"getHdStream".equals(call.method)) {
result.notImplemented();
return;
}
final String videoId = call.argument("videoId");
final Integer targetHeightArgument =
call.argument("targetHeight");
final String preferredLanguageArgument =
call.argument("preferredLanguage");
final int targetHeight =
targetHeightArgument != null
? targetHeightArgument
: 1080;
final String preferredLanguage =
preferredLanguageArgument != null
&& !preferredLanguageArgument.trim().isEmpty()
? preferredLanguageArgument.trim()
: "auto";
if (videoId == null || videoId.trim().isEmpty()) {
result.error(
"HD_BAD_VIDEO_ID",
"videoId manquant",
null
);
return;
}
hdExecutor.execute(() -> {
try {
final Map<String, Object> hdInfo =
HdStreamExtractor.getHdInfo(
videoId,
targetHeight,
preferredLanguage
);
runOnUiThread(
() -> result.success(hdInfo)
);
} catch (Throwable error) {
final String message =
error.getMessage() != null
? error.getMessage()
: error.getClass()
.getSimpleName();
runOnUiThread(
() -> result.error(
"HD_EXTRACTION_FAILED",
message,
error.getClass().getName()
)
);
}
});
}
);
deviceChannel = new MethodChannel(
flutterEngine
.getDartExecutor()
.getBinaryMessenger(),
DEVICE_CHANNEL_NAME
);
deviceChannel.setMethodCallHandler(
(MethodCall call, MethodChannel.Result result) -> {
switch (call.method) {
case "getSystemVolume":
result.success(getSystemVolume());
break;
case "setSystemVolume":
final Number volume = call.argument("value");
if (volume == null) {
result.error(
"BAD_VOLUME",
"Valeur de volume manquante",
null
);
break;
}
setSystemVolume(volume.doubleValue());
result.success(true);
break;
case "getWindowBrightness":
result.success(getWindowBrightness());
break;
case "setWindowBrightness":
final Number brightness = call.argument("value");
if (brightness == null) {
result.error(
"BAD_BRIGHTNESS",
"Valeur de luminosité manquante",
null
);
break;
}
setWindowBrightness(brightness.doubleValue());
result.success(true);
break;
case "resetWindowBrightness":
resetWindowBrightness();
result.success(true);
break;
case "setKeepScreenOn":
final Boolean keepOn = call.argument("value");
setKeepScreenOn(keepOn != null && keepOn);
result.success(true);
break;
case "schedulePauseScreenSleep":
final Number delayMs = call.argument("milliseconds");
schedulePauseScreenSleep(
delayMs != null
? delayMs.longValue()
: 0L
);
result.success(true);
break;
default:
result.notImplemented();
break;
}
}
);
cacheChannel = new MethodChannel(
flutterEngine
.getDartExecutor()
.getBinaryMessenger(),
CACHE_CHANNEL_NAME
);
cacheChannel.setMethodCallHandler(
(MethodCall call, MethodChannel.Result result) -> {
switch (call.method) {
case "getCachePaths":
result.success(getMediaCachePaths());
break;
case "getCacheSize":
final Integer sizeMode = call.argument("mode");
result.success(
directorySize(
getMediaCacheDirectory(
sizeMode != null ? sizeMode : 1
)
)
);
break;
case "clearCache":
final Integer clearMode = call.argument("mode");
clearMediaCache(
clearMode != null ? clearMode : 1
);
result.success(true);
break;
default:
result.notImplemented();
break;
}
}
);
registerScreenReceiver();
}
private Map<String, String> getMediaCachePaths() {
final Map<String, String> result = new LinkedHashMap<>();
result.put(
"internal",
getMediaCacheDirectory(1).getAbsolutePath()
);
result.put(
"external",
getMediaCacheDirectory(2).getAbsolutePath()
);
return result;
}
private File getMediaCacheDirectory(int mode) {
File base;
if (mode == 2) {
base = getExternalCacheDir();
if (base == null) {
base = getCacheDir();
}
} else {
base = getCacheDir();
}
final File directory = new File(
base,
"youtube_personal_media"
);
if (!directory.exists()) {
directory.mkdirs();
}
return directory;
}
private long directorySize(File file) {
if (file == null || !file.exists()) {
return 0L;
}
if (file.isFile()) {
return file.length();
}
long total = 0L;
final File[] children = file.listFiles();
if (children == null) {
return 0L;
}
for (final File child : children) {
total += directorySize(child);
}
return total;
}
private void clearMediaCache(int mode) {
final File directory = getMediaCacheDirectory(mode);
final File[] children = directory.listFiles();
if (children == null) {
return;
}
for (final File child : children) {
deleteRecursively(child);
}
}
private void deleteRecursively(File file) {
if (file == null || !file.exists()) {
return;
}
if (file.isDirectory()) {
final File[] children = file.listFiles();
if (children != null) {
for (final File child : children) {
deleteRecursively(child);
}
}
}
file.delete();
}
private double getSystemVolume() {
final AudioManager audioManager =
(AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return 0.5;
}
final int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
if (max <= 0) {
return 0.0;
}
return (double) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
/ (double) max;
}
private void setSystemVolume(double value) {
final AudioManager audioManager =
(AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return;
}
final double clamped = Math.max(0.0, Math.min(1.0, value));
final int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
final int next = (int) Math.round(clamped * max);
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
next,
0
);
}
private double getWindowBrightness() {
final WindowManager.LayoutParams attributes =
getWindow().getAttributes();
if (attributes.screenBrightness >= 0.0f) {
return Math.max(0.0, Math.min(1.0, attributes.screenBrightness));
}
try {
final int systemBrightness = Settings.System.getInt(
getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS
);
return Math.max(0.0, Math.min(1.0, systemBrightness / 255.0));
} catch (Settings.SettingNotFoundException ignored) {
return 0.5;
}
}
private void setWindowBrightness(double value) {
final double clamped = Math.max(0.02, Math.min(1.0, value));
final WindowManager.LayoutParams attributes =
getWindow().getAttributes();
attributes.screenBrightness = (float) clamped;
getWindow().setAttributes(attributes);
}
private void resetWindowBrightness() {
final WindowManager.LayoutParams attributes =
getWindow().getAttributes();
attributes.screenBrightness =
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
getWindow().setAttributes(attributes);
}
private void setKeepScreenOn(boolean keepOn) {
cancelPauseScreenSleep();
applyKeepScreenOn(keepOn);
}
private void applyKeepScreenOn(boolean keepOn) {
if (keepOn) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
);
getWindow().getDecorView().setKeepScreenOn(true);
} else {
getWindow().getDecorView().setKeepScreenOn(false);
getWindow().clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
);
}
}
private void schedulePauseScreenSleep(long delayMs) {
cancelPauseScreenSleep();
if (delayMs <= 0L) {
applyKeepScreenOn(true);
Log.i(
"YouTubePersonal",
"[ANDROID-SCREEN][18.7.19] pause: veille désactivée"
);
return;
}
applyKeepScreenOn(true);
pauseSleepRunnable = () -> {
applyKeepScreenOn(false);
pauseSleepRunnable = null;
Log.i(
"YouTubePersonal",
"[ANDROID-SCREEN][18.7.19] délai pause écoulé -> keepOn=false"
);
if (pipChannel != null) {
pipChannel.invokeMethod(
"pauseScreenSleepReleased",
true
);
}
};
screenPolicyHandler.postDelayed(
pauseSleepRunnable,
delayMs
);
Log.i(
"YouTubePersonal",
"[ANDROID-SCREEN][18.7.19] timer pause=" + delayMs + "ms"
);
}
private void cancelPauseScreenSleep() {
if (pauseSleepRunnable == null) {
return;
}
screenPolicyHandler.removeCallbacks(pauseSleepRunnable);
pauseSleepRunnable = null;
}
private String getOrientationLabel(Configuration configuration) {
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
? "landscape"
: "portrait";
}
private void enterPip(MethodChannel.Result result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
result.success(false);
return;
}
final PictureInPictureParams params =
new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(16, 9))
.build();
result.success(
enterPictureInPictureMode(params)
);
}
private void registerScreenReceiver() {
if (screenReceiverRegistered) {
return;
}
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(
screenReceiver,
filter,
Context.RECEIVER_NOT_EXPORTED
);
} else {
registerReceiver(
screenReceiver,
filter
);
}
screenReceiverRegistered = true;
}
private void requestNotificationPermission(
MethodChannel.Result result
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
result.success(true);
return;
}
if (
checkSelfPermission(
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
result.success(true);
return;
}
if (pendingNotificationPermissionResult != null) {
result.success(false);
return;
}
pendingNotificationPermissionResult = result;
requestPermissions(
new String[] {
Manifest.permission.POST_NOTIFICATIONS
},
NOTIFICATION_PERMISSION_REQUEST_CODE
);
}
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
super.onRequestPermissionsResult(
requestCode,
permissions,
grantResults
);
if (
requestCode
!= NOTIFICATION_PERMISSION_REQUEST_CODE
) {
return;
}
final boolean granted =
grantResults.length > 0
&& grantResults[0]
== PackageManager.PERMISSION_GRANTED;
if (pendingNotificationPermissionResult != null) {
pendingNotificationPermissionResult.success(granted);
pendingNotificationPermissionResult = null;
}
}
@Override
public void onConfigurationChanged(
@NonNull Configuration newConfig
) {
super.onConfigurationChanged(newConfig);
if (pipChannel != null) {
final String orientation = getOrientationLabel(newConfig);
pipChannel.invokeMethod(
"orientationChanged",
orientation
);
Log.i(
"YouTubePersonal",
"[ANDROID-ORIENTATION][18.7.19] " + orientation
);
}
}
@Override
public void onPictureInPictureModeChanged(
boolean isInPictureInPictureMode,
@NonNull Configuration newConfig
) {
super.onPictureInPictureModeChanged(
isInPictureInPictureMode,
newConfig
);
if (pipChannel != null) {
pipChannel.invokeMethod(
"pipChanged",
isInPictureInPictureMode
);
}
}
@Override
protected void onDestroy() {
cancelPauseScreenSleep();
applyKeepScreenOn(false);
resetWindowBrightness();
if (screenReceiverRegistered) {
try {
unregisterReceiver(screenReceiver);
} catch (Exception ignored) {
// Receiver déjà détaché.
}
screenReceiverRegistered = false;
}
pipChannel = null;
hdChannel = null;
deviceChannel = null;
hdExecutor.shutdownNow();
super.onDestroy();
}
}
@@ -0,0 +1,86 @@
package com.studioweb.youtube_personal;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
/**
* Minimal downloader required by NewPipeExtractor.
*
* No Google account cookie and no OAuth token is injected here.
* The authenticated YouTube Data API path remains completely separate.
*/
public final class NewPipeDownloader extends Downloader {
private static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) "
+ "Gecko/20100101 Firefox/140.0";
private final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
@Override
public Response execute(final Request request)
throws IOException, ReCaptchaException {
final String httpMethod = request.httpMethod();
final String url = request.url();
final Map<String, List<String>> headers =
request.headers();
final byte[] dataToSend = request.dataToSend();
RequestBody requestBody = null;
if (dataToSend != null) {
requestBody = RequestBody.create(dataToSend);
}
final okhttp3.Request.Builder builder =
new okhttp3.Request.Builder()
.method(httpMethod, requestBody)
.url(url)
.addHeader("User-Agent", USER_AGENT);
headers.forEach((name, values) -> {
builder.removeHeader(name);
for (final String value : values) {
builder.addHeader(name, value);
}
});
try (okhttp3.Response response =
client.newCall(builder.build()).execute()) {
if (response.code() == 429) {
throw new ReCaptchaException(
"YouTube rate-limit / challenge",
url
);
}
String responseBody = null;
final ResponseBody body = response.body();
if (body != null) {
responseBody = body.string();
}
return new Response(
response.code(),
response.message(),
response.headers().toMultimap(),
responseBody,
response.request().url().toString()
);
}
}
}
@@ -0,0 +1,11 @@
package com.studioweb.youtube_personal
/*
* MainActivity a été déplacée vers:
*
* android/app/src/main/java/com/studioweb/youtube_personal/MainActivity.java
*
* Le module Android actuel n'incluait pas MainActivity.kt dans l'APK.
* Ce fichier reste volontairement sans classe MainActivity pour éviter
* tout doublon si la compilation Kotlin est réactivée plus tard.
*/
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+48
View File
@@ -0,0 +1,48 @@
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
// BEGIN YOUTUBE_PERSONAL_AGP9_KOTLIN_BRIDGE_18_10_6
// Flutter 3.44 + AGP 9 peut fonctionner temporairement avec
// android.builtInKotlin=false. Certains plugins regardent uniquement la
// version d'AGP et n'appliquent alors plus KGP : leurs sources .kt ne sont
// jamais compilees et GeneratedPluginRegistrant ne trouve plus les classes.
val youtubePersonalLegacyKotlinPlugins = setOf(
"package_info_plus",
"wakelock_plus",
)
subprojects {
if (name in youtubePersonalLegacyKotlinPlugins) {
val pluginProject = this
pluginManager.withPlugin("com.android.library") {
if (!pluginProject.pluginManager.hasPlugin("org.jetbrains.kotlin.android")) {
pluginProject.pluginManager.apply("org.jetbrains.kotlin.android")
}
}
}
}
// END YOUTUBE_PERSONAL_AGP9_KOTLIN_BRIDGE_18_10_6
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -0,0 +1,26 @@
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
# YouTube Personal - Android memory build fix
org.gradle.jvmargs=-Xmx16384m -XX:MaxMetaspaceSize=4096m -Dfile.encoding=UTF-8
org.gradle.daemon=true
org.gradle.parallel=false
org.gradle.caching=true
org.gradle.workers.max=4
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
@@ -0,0 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")