Initial upload: insta-tv-viewer

This commit is contained in:
Abderrahim-Mouhcine
2026-08-16 19:58:01 +01:00
commit 142cdcfc26
145 changed files with 6773 additions and 0 deletions
+785
View File
@@ -0,0 +1,785 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
void main() {
runApp(const InstaTvApp());
}
enum EditTarget { username, password }
enum TvAction { scrollUp, scrollDown, playSound, fullscreen, exitFullscreen }
class TvControl {
final String label;
final TvAction action;
const TvControl(this.label, this.action);
}
class InstaTvApp extends StatefulWidget {
const InstaTvApp({super.key});
@override
State<InstaTvApp> createState() => _InstaTvAppState();
}
class _InstaTvAppState extends State<InstaTvApp> {
late final WebViewController controller;
final FocusNode mainFocusNode = FocusNode();
final FocusNode inputFocusNode = FocusNode();
final TextEditingController inputController = TextEditingController();
double cursorX = 280;
double cursorY = 180;
String usernameValue = '';
String passwordValue = '';
String lastAction = 'prêt';
EditTarget? editTarget;
bool videoFullscreen = false;
static const double step = 35;
static const double cursorSize = 18;
static const double controlsLeft = 16;
static const double controlsTop = 72;
static const double controlWidth = 260;
static const double controlHeight = 58;
static const double controlGap = 10;
final List<TvControl> controls = const [
TvControl('Scroll haut', TvAction.scrollUp),
TvControl('Scroll bas', TvAction.scrollDown),
TvControl('Play / Son', TvAction.playSound),
TvControl('Fullscreen', TvAction.fullscreen),
TvControl('Sortir full', TvAction.exitFullscreen),
];
@override
void initState() {
super.initState();
final c = WebViewController();
if (c.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(c.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}
c
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) => injectPageHelpers(),
onNavigationRequest: (request) {
final url = request.url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(
Uri.parse('https://www.instagram.com/accounts/login/?hl=fr'),
);
controller = c;
WidgetsBinding.instance.addPostFrameCallback((_) {
mainFocusNode.requestFocus();
});
}
@override
void dispose() {
mainFocusNode.dispose();
inputFocusNode.dispose();
inputController.dispose();
super.dispose();
}
Future<void> injectPageHelpers() async {
const js = '''
(() => {
if (window.__instaTvHelpersInstalled) return;
window.__instaTvHelpersInstalled = true;
const style = document.createElement('style');
style.textContent = `
video.__instaTvFullscreenVideo {
position: fixed !important;
inset: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
object-fit: contain !important;
background: black !important;
z-index: 2147483000 !important;
}
body.__instaTvNoScroll {
overflow: hidden !important;
}
`;
document.head.appendChild(style);
window.__instaTvGetBestVideo = () => {
const fullscreen = document.querySelector('video.__instaTvFullscreenVideo');
if (fullscreen) return fullscreen;
const videos = Array.from(document.querySelectorAll('video'));
let best = null;
let bestArea = 0;
for (const v of videos) {
const r = v.getBoundingClientRect();
const visible =
r.width > 80 &&
r.height > 80 &&
r.bottom > 0 &&
r.right > 0 &&
r.top < window.innerHeight &&
r.left < window.innerWidth;
if (!visible) continue;
const area = r.width * r.height;
if (area > bestArea) {
best = v;
bestArea = area;
}
}
return best;
};
})();
''';
await controller.runJavaScript(js);
}
bool isOkKey(KeyEvent event) {
final logical = event.logicalKey;
final logicalName = logical.debugName?.toLowerCase() ?? '';
final physicalName = event.physicalKey.debugName?.toLowerCase() ?? '';
return logical == LogicalKeyboardKey.select ||
logical == LogicalKeyboardKey.enter ||
logical == LogicalKeyboardKey.numpadEnter ||
logical == LogicalKeyboardKey.space ||
logicalName.contains('select') ||
logicalName.contains('enter') ||
logicalName.contains('center') ||
logicalName.contains('accept') ||
logicalName.contains('button a') ||
physicalName.contains('select') ||
physicalName.contains('enter') ||
physicalName.contains('center') ||
physicalName.contains('accept') ||
physicalName.contains('button a');
}
KeyEventResult handleMainKey(KeyEvent event, BoxConstraints box) {
if (event is! KeyDownEvent) return KeyEventResult.handled;
final key = event.logicalKey;
if (key == LogicalKeyboardKey.arrowLeft) {
setState(() {
cursorX = (cursorX - step).clamp(0, box.maxWidth - cursorSize);
lastAction = 'gauche';
});
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.arrowRight) {
setState(() {
cursorX = (cursorX + step).clamp(0, box.maxWidth - cursorSize);
lastAction = 'droite';
});
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.arrowUp) {
setState(() {
cursorY = (cursorY - step).clamp(0, box.maxHeight - cursorSize);
lastAction = 'haut';
});
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.arrowDown) {
setState(() {
cursorY = (cursorY + step).clamp(0, box.maxHeight - cursorSize);
lastAction = 'bas';
});
return KeyEventResult.handled;
}
if (isOkKey(event)) {
handleOk(box);
return KeyEventResult.handled;
}
if (key == LogicalKeyboardKey.goBack ||
key == LogicalKeyboardKey.escape ||
key == LogicalKeyboardKey.browserBack) {
goBack();
return KeyEventResult.handled;
}
return KeyEventResult.handled;
}
TvAction? hitTestControl() {
final centerX = cursorX + cursorSize / 2;
final centerY = cursorY + cursorSize / 2;
for (int i = 0; i < controls.length; i++) {
final top = controlsTop + i * (controlHeight + controlGap);
final bottom = top + controlHeight;
if (centerX >= controlsLeft &&
centerX <= controlsLeft + controlWidth &&
centerY >= top &&
centerY <= bottom) {
return controls[i].action;
}
}
return null;
}
Future<void> handleOk(BoxConstraints box) async {
final action = hitTestControl();
if (action != null) {
await runControlAction(action);
return;
}
final loginVisible = await isLoginFormVisible();
final yRatio = cursorY / box.maxHeight;
if (loginVisible && cursorX > 300 && yRatio >= 0.30 && yRatio < 0.46) {
openOverlayInput(EditTarget.username);
return;
}
if (loginVisible && cursorX > 300 && yRatio >= 0.46 && yRatio < 0.57) {
openOverlayInput(EditTarget.password);
return;
}
setState(() {
lastAction = 'clic page';
});
clickAtCursor(box);
}
Future<bool> isLoginFormVisible() async {
const js = '''
(() => {
return !!(
document.querySelector('input[name="username"]') &&
document.querySelector('input[type="password"]')
);
})();
''';
final result = await controller.runJavaScriptReturningResult(js);
return result.toString().contains('true');
}
Future<void> runControlAction(TvAction action) async {
switch (action) {
case TvAction.scrollUp:
await scrollPage(-760);
break;
case TvAction.scrollDown:
await scrollPage(760);
break;
case TvAction.playSound:
await playBestVideo();
break;
case TvAction.fullscreen:
await fullscreenBestVideo();
break;
case TvAction.exitFullscreen:
await exitVideoFullscreen();
break;
}
}
Future<void> scrollPage(int amount) async {
setState(() {
lastAction = amount > 0 ? 'scroll bas' : 'scroll haut';
});
final js = '''
(() => {
const amount = $amount;
window.scrollBy({ top: amount, left: 0, behavior: 'smooth' });
const candidates = [
document.scrollingElement,
document.documentElement,
document.body,
...Array.from(document.querySelectorAll('main, div, section'))
].filter(Boolean);
candidates.forEach(el => {
try {
if (el.scrollHeight > el.clientHeight + 80) {
el.scrollBy({ top: amount, left: 0, behavior: 'smooth' });
el.scrollTop += amount;
}
} catch(e) {}
});
})();
''';
await controller.runJavaScript(js);
}
Future<void> playBestVideo() async {
setState(() {
lastAction = 'play + son';
});
const js = '''
(() => {
const v = window.__instaTvGetBestVideo?.() ||
Array.from(document.querySelectorAll('video'))[0];
if (!v) return;
v.muted = false;
v.defaultMuted = false;
v.volume = 1;
v.playsInline = false;
v.setAttribute('playsinline', 'false');
const r = v.getBoundingClientRect();
const x = Math.round(r.left + r.width / 2);
const y = Math.round(r.top + r.height / 2);
['pointerdown', 'mousedown', 'mouseup', 'pointerup', 'click'].forEach(type => {
v.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
}));
});
if (v.paused) {
v.play().catch(() => {});
}
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
const muteBtn = buttons.find(b => {
const txt = (b.innerText || b.getAttribute('aria-label') || '').toLowerCase();
return txt.includes('unmute') ||
txt.includes('activer le son') ||
txt.includes('sound') ||
txt.includes('audio') ||
txt.includes('son');
});
if (muteBtn) muteBtn.click();
})();
''';
await controller.runJavaScript(js);
}
Future<void> fullscreenBestVideo() async {
setState(() {
videoFullscreen = true;
lastAction = 'fullscreen';
});
const js = '''
(() => {
document.querySelectorAll('video.__instaTvFullscreenVideo').forEach(v => {
v.classList.remove('__instaTvFullscreenVideo');
});
const v = window.__instaTvGetBestVideo?.() ||
Array.from(document.querySelectorAll('video'))[0];
if (!v) return;
v.muted = false;
v.defaultMuted = false;
v.volume = 1;
v.playsInline = false;
v.setAttribute('playsinline', 'false');
v.classList.add('__instaTvFullscreenVideo');
document.body.classList.add('__instaTvNoScroll');
setTimeout(() => {
v.play().catch(() => {});
}, 250);
})();
''';
await controller.runJavaScript(js);
}
Future<void> exitVideoFullscreen() async {
setState(() {
videoFullscreen = false;
lastAction = 'sortie fullscreen';
});
const js = '''
(() => {
document.querySelectorAll('video.__instaTvFullscreenVideo').forEach(v => {
v.classList.remove('__instaTvFullscreenVideo');
});
document.body.classList.remove('__instaTvNoScroll');
})();
''';
await controller.runJavaScript(js);
}
void openOverlayInput(EditTarget target) {
setState(() {
editTarget = target;
inputController.text =
target == EditTarget.username ? usernameValue : passwordValue;
lastAction =
target == EditTarget.username ? 'édition identifiant' : 'édition mot de passe';
});
WidgetsBinding.instance.addPostFrameCallback((_) {
inputFocusNode.requestFocus();
SystemChannels.textInput.invokeMethod('TextInput.show');
});
}
Future<void> saveOverlayInput() async {
final target = editTarget;
if (target == null) return;
final value = inputController.text;
if (target == EditTarget.username) {
usernameValue = value;
await injectLoginValue(value, isPassword: false);
} else {
passwordValue = value;
await injectLoginValue(value, isPassword: true);
}
setState(() {
editTarget = null;
lastAction = 'texte injecté';
});
mainFocusNode.requestFocus();
}
void cancelOverlayInput() {
setState(() {
editTarget = null;
lastAction = 'saisie annulée';
});
mainFocusNode.requestFocus();
}
Future<void> injectLoginValue(String value, {required bool isPassword}) async {
final encodedValue = jsonEncode(value);
final target = isPassword ? 'password' : 'username';
final js = '''
(() => {
const value = $encodedValue;
const target = '$target';
let el = null;
if (target === 'password') {
el = document.querySelector('input[type="password"]');
} else {
el =
document.querySelector('input[name="username"]') ||
document.querySelector('input[autocomplete="username"]') ||
document.querySelector('input[type="text"]') ||
document.querySelector('input');
}
if (!el) return;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
)?.set;
if (setter) {
setter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.blur();
})();
''';
await controller.runJavaScript(js);
}
Future<void> clickAtCursor(BoxConstraints box) async {
final clickX = cursorX + cursorSize / 2;
final clickY = cursorY + cursorSize / 2;
final js = '''
(() => {
const x = Math.round($clickX / ${box.maxWidth} * window.innerWidth);
const y = Math.round($clickY / ${box.maxHeight} * window.innerHeight);
let el = document.elementFromPoint(x, y);
if (!el) return;
const video = el.closest('video');
if (video) {
video.muted = false;
video.volume = 1;
['pointerdown', 'mousedown', 'mouseup', 'pointerup', 'click'].forEach(type => {
video.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
}));
});
if (video.paused) {
video.play().catch(() => {});
}
return;
}
const clickable = el.closest('a, button, [role="button"]') || el;
clickable.focus();
['pointerover', 'pointerdown', 'mousedown', 'mouseup', 'pointerup', 'click'].forEach(type => {
clickable.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
}));
});
if (clickable.tagName === 'A' && clickable.href) {
window.location.href = clickable.href;
} else {
clickable.click();
}
})();
''';
await controller.runJavaScript(js);
}
Future<void> goBack() async {
if (editTarget != null) {
cancelOverlayInput();
return;
}
if (videoFullscreen) {
await exitVideoFullscreen();
return;
}
if (await controller.canGoBack()) {
await controller.goBack();
} else {
await controller.loadRequest(
Uri.parse('https://www.instagram.com/accounts/login/?hl=fr'),
);
}
}
Widget buildControls() {
return Positioned(
left: controlsLeft,
top: controlsTop,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final control in controls)
Container(
width: controlWidth,
height: controlHeight,
margin: const EdgeInsets.only(bottom: controlGap),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.78),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white30),
),
child: Text(
control.label,
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
Widget buildInputOverlay() {
final target = editTarget;
if (target == null) return const SizedBox.shrink();
final isPassword = target == EditTarget.password;
return Container(
color: Colors.black.withOpacity(0.72),
child: Center(
child: Container(
width: 760,
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
color: const Color(0xFF202020),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white24),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isPassword ? 'Mot de passe Instagram' : 'Identifiant Instagram',
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
TextField(
controller: inputController,
focusNode: inputFocusNode,
autofocus: true,
obscureText: isPassword,
style: const TextStyle(color: Colors.white, fontSize: 24),
keyboardType:
isPassword ? TextInputType.visiblePassword : TextInputType.text,
decoration: const InputDecoration(
filled: true,
fillColor: Colors.white10,
border: OutlineInputBorder(),
hintText: 'Écrire ici',
hintStyle: TextStyle(color: Colors.white54),
),
onSubmitted: (_) => saveOverlayInput(),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: cancelOverlayInput,
child: const Text('Annuler', style: TextStyle(fontSize: 22)),
),
const SizedBox(width: 18),
FilledButton(
onPressed: saveOverlayInput,
child: const Text('Valider', style: TextStyle(fontSize: 22)),
),
],
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Insta TV Viewer',
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: LayoutBuilder(
builder: (context, box) {
return Focus(
autofocus: true,
focusNode: mainFocusNode,
onKeyEvent: (node, event) => handleMainKey(event, box),
child: Stack(
children: [
IgnorePointer(child: WebViewWidget(controller: controller)),
buildControls(),
Positioned(
left: 12,
top: 12,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.black.withOpacity(0.75),
child: Text(
'Action: $lastAction',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
),
if (editTarget == null)
Positioned(
left: cursorX,
top: cursorY,
child: Container(
width: cursorSize,
height: cursorSize,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
buildInputOverlay(),
],
),
);
},
),
),
),
);
}
}