mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
Align the Android app with xtremflow on three fronts. EPG: add XmltvService, a streaming XMLTV parser (gunzip-aware, indexed to a -3h/+18h window, JSON disk cache with a 6h TTL). XtreamServiceMobile now falls back through the provider's xmltv.php, a user-supplied URL, then a community guide whenever get_short_epg returns nothing. Channels are matched on epg_channel_id first, then on a normalized name (accents, HD/FHD/4K suffixes, "FR:" and "|FR|" prefixes). A streamId -> Channel index populated by getLiveChannels supplies those keys, so no getShortEPG call site changed. EPG requests are also funnelled through a 3-slot semaphore, as in xtremflow: a 30-tile grid used to fire 30 parallel player_api.php calls, which rate-limited panels answered with errors, emptying the guide for the whole screen. Theme: port the Warm Cinema charter (ember/charcoal/cream, Fraunces + Karla) into app_colors, app_theme and app_decorations. The 21 Apple TV colour names already in use are kept as compatibility aliases. mobile_theme now derives from AppTheme instead of duplicating its 390 lines. Playback lifecycle: stop reacting to AppLifecycleState.inactive, which fires on transient Android interruptions such as pulling down the notification shade and was killing playback. Both engines now release on paused/hidden/detached via _releasePlayback(): stream stopped, decoder released, timers cancelled, wakelock dropped. An _isReleased latch keeps the watchdog and reconnect paths from restarting a stream from the background. Long-run stability: cancel the six player stream subscriptions on dispose, cap demuxer-max-back-bytes at 8MB (mpv defaults to 50MiB, which combined with the 150MB readahead pushed the native heap toward an Android memory kill after a few hours), rebuild on position only while the controls are visible, and dispose the Lite player's focus nodes. Startup: prewarmHost() pre-resolves DNS and opens then closes a bare TCP/TLS socket before playback. No HTTP request is issued, so no provider connection slot is consumed. Capped at 1.5s. Verified: flutter analyze clean on all touched files; 19/19 tests, including a run over a real 46MB/69802-programme dump (517 channels indexed in 1.1s, 5/5 name probes matched); flutter build apk --release succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
2.9 KiB
Dart
81 lines
2.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/theme/app_colors.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
|
|
/// XtremFlow Mobile Theme — "Warm Cinema", phone-scaled.
|
|
///
|
|
/// Derived from [AppTheme] rather than redefined: the palette, typography and
|
|
/// component styling stay in one place, and this layer only shrinks the type
|
|
/// scale and tightens paddings for a handset.
|
|
class MobileTheme {
|
|
MobileTheme._();
|
|
|
|
// ============ SPACING ============
|
|
static const double spacing4 = AppTheme.spacing4;
|
|
static const double spacing8 = AppTheme.spacing8;
|
|
static const double spacing12 = AppTheme.spacing12;
|
|
static const double spacing16 = AppTheme.spacing16;
|
|
static const double spacing24 = AppTheme.spacing24;
|
|
|
|
// ============ RADIUS ============
|
|
static const double radiusSm = AppTheme.radiusSm;
|
|
static const double radiusMd = AppTheme.radiusMd;
|
|
static const double radiusLg = AppTheme.radiusLg;
|
|
|
|
/// Phone type scale: display sizes come down ~20%, body stays legible.
|
|
static TextTheme _mobileTextTheme(TextTheme base) {
|
|
TextStyle? shrink(TextStyle? style, double factor) {
|
|
if (style?.fontSize == null) return style;
|
|
return style!.copyWith(fontSize: style.fontSize! * factor);
|
|
}
|
|
|
|
return base.copyWith(
|
|
displayLarge: shrink(base.displayLarge, 0.70),
|
|
displayMedium: shrink(base.displayMedium, 0.76),
|
|
displaySmall: shrink(base.displaySmall, 0.84),
|
|
headlineLarge: shrink(base.headlineLarge, 0.78),
|
|
headlineMedium: shrink(base.headlineMedium, 0.86),
|
|
headlineSmall: shrink(base.headlineSmall, 0.92),
|
|
titleLarge: shrink(base.titleLarge, 0.94),
|
|
bodyLarge: shrink(base.bodyLarge, 0.90),
|
|
bodyMedium: shrink(base.bodyMedium, 0.92),
|
|
);
|
|
}
|
|
|
|
static ThemeData _scaled(ThemeData base) {
|
|
return base.copyWith(
|
|
textTheme: _mobileTextTheme(base.textTheme),
|
|
inputDecorationTheme: base.inputDecorationTheme.copyWith(
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: spacing16,
|
|
vertical: spacing12,
|
|
),
|
|
),
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: base.filledButtonTheme.style?.copyWith(
|
|
padding: const WidgetStatePropertyAll(
|
|
EdgeInsets.symmetric(horizontal: spacing16, vertical: spacing12),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ============ DARK THEME (Mobile Main) ============
|
|
static ThemeData get darkTheme => _scaled(AppTheme.darkTheme);
|
|
|
|
// ============ LIGHT THEME ============
|
|
static ThemeData get lightTheme => _scaled(AppTheme.lightTheme);
|
|
|
|
/// Returns the appropriate MobileTheme based on the current [BuildContext]
|
|
/// brightness.
|
|
static ThemeData themeOf(BuildContext context) {
|
|
final brightness = Theme.of(context).brightness;
|
|
return brightness == Brightness.dark ? darkTheme : lightTheme;
|
|
}
|
|
|
|
/// Accent used for active/selected affordances on mobile.
|
|
static const Color accent = AppColors.primaryContainer;
|
|
}
|