refactor: strip obvious comments

This commit is contained in:
edde746
2026-05-04 22:40:18 +02:00
parent fdaff3687d
commit ed4be7b96d
162 changed files with 208 additions and 1440 deletions
+1 -16
View File
@@ -35,17 +35,12 @@ class LogEntry {
/// Estimate the memory size of this log entry in bytes
int get estimatedSize {
int size = 0;
// DateTime: ~8 bytes
size += 8;
// Level enum: ~4 bytes
size += 4;
// Message string: 2 bytes per character (UTF-16)
size += message.length * 2;
// Error string: 2 bytes per character if present
if (error != null) {
size += error.toString().length * 2;
}
// Stack trace string: 2 bytes per character if present
if (stackTrace != null) {
size += stackTrace.toString().length * 2;
}
@@ -58,25 +53,21 @@ class LogEntry {
/// Storage is handled by [MemoryAwareLogPrinter.log()] — this class only
/// forwards formatted lines to the console via the default [ConsoleOutput].
class MemoryLogOutput extends LogOutput {
static const int maxLogSizeBytes = 5 * 1024 * 1024; // 5 MB
static const int maxLogSizeBytes = 5 * 1024 * 1024;
static final ListQueue<LogEntry> _logs = ListQueue<LogEntry>();
static int _currentSize = 0;
static final _consoleOutput = ConsoleOutput();
/// Get all stored logs (newest first)
static List<LogEntry> getLogs() => _logs.toList().reversed.toList();
/// Clear all stored logs
static void clearLogs() {
_logs.clear();
_currentSize = 0;
}
/// Get current log buffer size in bytes
static int getCurrentSize() => _currentSize;
/// Get current log buffer size in MB
static double getCurrentSizeMB() => _currentSize / (1024 * 1024);
@override
@@ -115,7 +106,6 @@ class MemoryAwareLogPrinter extends LogPrinter {
MemoryLogOutput._currentSize -= removed.estimatedSize;
}
// Delegate a redacted event to the wrapped printer for console output.
return _wrappedPrinter.log(
LogEvent(event.level, message, time: event.time, error: error, stackTrace: event.stackTrace),
);
@@ -136,7 +126,6 @@ class ProductionFilter extends LogFilter {
}
}
/// Global filter instance
final _productionFilter = ProductionFilter();
/// Centralized logger instance for the application.
@@ -161,13 +150,9 @@ Logger appLogger = Logger(
void setLoggerLevel(bool debugEnabled) {
final newLevel = debugEnabled ? Level.debug : Level.info;
// Update the filter level
_productionFilter.setLevel(newLevel);
// Recreate the logger instance with the new level
// This ensures it works in release mode where Logger.level might be optimized away
appLogger = Logger(printer: MemoryAwareLogPrinter(SimplePrinter()), filter: _productionFilter, level: newLevel);
// Also set the static level for consistency
Logger.level = newLevel;
}
-4
View File
@@ -5,10 +5,6 @@
class CodecUtils {
CodecUtils._();
/// Maps Plex subtitle codec names to file extensions.
///
/// Returns the appropriate file extension for a given subtitle codec.
/// Defaults to 'srt' for unknown or null codecs.
static String getSubtitleExtension(String? codec) {
if (codec == null) return 'srt';
-6
View File
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
/// Content type constants used throughout the app
class ContentTypes {
ContentTypes._();
@@ -21,17 +20,13 @@ class ContentTypes {
static const Set<String> playableTypes = {movie, episode, clip, track};
}
/// Utility class for content type checking and filtering
class ContentTypeHelper {
ContentTypeHelper._();
/// Checks if the given type is music content (artist, album, or track)
static bool isMusicContent(String type) => ContentTypes.musicTypes.contains(type.toLowerCase());
/// Checks if the given type is video content (movie, show, episode, or season)
static bool isVideoContent(String type) => ContentTypes.videoTypes.contains(type.toLowerCase());
/// Checks if the given [MediaLibrary] is a music library.
static bool isMusicLibrary(dynamic lib) {
if (lib == null) return false;
try {
@@ -43,7 +38,6 @@ class ContentTypeHelper {
}
}
/// Returns the appropriate icon for a given library type
static IconData getLibraryIcon(String type) {
switch (type.toLowerCase()) {
case ContentTypes.movie:
-3
View File
@@ -60,10 +60,8 @@ class DeletionNotifier extends BaseNotifier<DeletionEvent> {
DeletionNotifier._internal();
/// Filter for events affecting a specific server
Stream<DeletionEvent> forServer(String serverId) => stream.where((e) => e.serverId == serverId);
/// Filter for events affecting a specific item or its children
Stream<DeletionEvent> forItem(String itemId) => stream.where((e) => e.affectsItem(itemId));
/// Emit a deletion event with logging
@@ -73,7 +71,6 @@ class DeletionNotifier extends BaseNotifier<DeletionEvent> {
super.notify(event);
}
/// Helper to emit a deletion event from a [MediaItem].
void notifyDeletedItem({required MediaItem item, bool isDownloadOnly = false}) {
notify(
DeletionEvent(
-6
View File
@@ -43,12 +43,10 @@ class DesktopAppBarHelper {
rightPadding = DesktopWindowPadding.mobileRight;
}
// If no platform-specific padding needed, return original actions
if (rightPadding == null) {
return actions;
}
// Add padding to keep actions away from edge
return actions != null ? [...actions, SizedBox(width: rightPadding)] : [SizedBox(width: rightPadding)];
}
@@ -61,7 +59,6 @@ class DesktopAppBarHelper {
return leading;
}
// Skip left padding when side navigation scope is present in widget tree
if (context != null && SideNavigationScope.isPresent(context)) {
if (includeGestureDetector) {
return GestureDetector(
@@ -120,7 +117,6 @@ class DesktopAppBarHelper {
return null;
}
// Skip extra width when side navigation scope is present in widget tree
if (context != null && SideNavigationScope.isPresent(context)) {
return null;
}
@@ -165,8 +161,6 @@ class DesktopTitleBarPadding extends StatelessWidget {
return child;
}
// Skip left padding when side navigation scope is present in widget tree
// (side nav already handles the traffic lights area)
if (SideNavigationScope.isPresent(context)) {
final right = rightPadding ?? 0.0;
if (right == 0.0) {
-2
View File
@@ -10,8 +10,6 @@ import '../widgets/dialog_action_button.dart';
import '../widgets/focusable_list_tile.dart';
import 'focus_utils.dart';
/// Utility functions for showing common dialogs
const _buttonPadding = EdgeInsets.symmetric(horizontal: 18, vertical: 14);
const _buttonShape = StadiumBorder();
-1
View File
@@ -102,7 +102,6 @@ Future<DownloadResult?> showDownloadOptionsAndQueue(
maxCount = customCount;
}
// For unwatched-based options on shows, offer sync vs one-time download
if (filter == DownloadFilter.unwatched && kind == MediaKind.show && context.mounted) {
final syncChoice = await showOptionPickerDialog<_SyncChoice>(
context,
-1
View File
@@ -1,6 +1,5 @@
import 'package:flutter/widgets.dart';
/// Utility class for common focus operations
class FocusUtils {
FocusUtils._();
-22
View File
@@ -10,7 +10,6 @@ String padNumber(int number, int width) {
return number.toString().padLeft(width, '0');
}
/// Utility class for formatting byte sizes and speeds
class ByteFormatter {
ByteFormatter._();
@@ -19,9 +18,6 @@ class ByteFormatter {
static const int _gb = _mb * 1024;
/// Format bytes to human-readable string (e.g., "1.5 GB", "256.3 MB")
///
/// [bytes] The number of bytes to format
/// [decimals] Number of decimal places (default: 1 for KB/MB, 2 for GB)
static String formatBytes(int bytes, {int? decimals}) {
if (bytes < _kb) return '$bytes B';
if (bytes < _mb) {
@@ -34,8 +30,6 @@ class ByteFormatter {
}
/// Format speed in bytes per second to human-readable string
///
/// [bytesPerSecond] The speed in bytes per second
static String formatSpeed(double bytesPerSecond) {
if (bytesPerSecond < _kb) {
return '${bytesPerSecond.toStringAsFixed(0)} B/s';
@@ -47,8 +41,6 @@ class ByteFormatter {
}
/// Format bitrate in kbps to human-readable string
///
/// [kbps] The bitrate in kilobits per second
static String formatBitrate(int kbps) {
if (kbps < 1000) return '$kbps kbps';
return '${(kbps / 1000).toStringAsFixed(1)} Mbps';
@@ -63,17 +55,13 @@ class ByteFormatter {
String formatDurationTextual(int milliseconds, {bool abbreviated = true}) {
final duration = Duration(milliseconds: milliseconds);
// Get the appropriate locale for the duration package
final durationLocale = _getDurationLocale();
// Format with abbreviated or full units (h, m) but no seconds
return prettyDuration(
duration,
abbreviated: abbreviated,
locale: durationLocale,
delimiter: abbreviated ? ' ' : ', ',
spacer: '',
// Configure to show only hours and minutes
tersity: DurationTersity.minute,
);
}
@@ -87,14 +75,12 @@ String formatDurationWithSeconds(Duration duration) {
// Get the appropriate locale for the duration package
final durationLocale = _getDurationLocale();
// Format with abbreviated units (h, m, s) including seconds
return prettyDuration(
duration,
abbreviated: true,
locale: durationLocale,
delimiter: ' ',
spacer: '',
// Show all non-zero units
tersity: DurationTersity.second,
);
}
@@ -105,7 +91,6 @@ String formatDurationWithSeconds(Duration duration) {
///
/// Used for: video controls, chapters, episode durations.
String formatDurationTimestamp(Duration duration) {
// Handle negative durations
final isNegative = duration.isNegative;
final absoluteDuration = duration.abs();
@@ -130,13 +115,11 @@ String formatSyncOffset(double offsetMs) {
final durationLocale = _getDurationLocale();
if (absMs >= 10000) {
// For values >= 10s, show decimal seconds (e.g., "+15.1s")
final seconds = (offsetMs.abs() / 1000).toStringAsFixed(1);
final unit = durationLocale.second(1, true);
return '$sign$seconds$unit';
}
// For values < 10s, show milliseconds (e.g., "+7300ms")
final unit = durationLocale.millisecond(1, true);
return '$sign$absMs$unit';
}
@@ -144,7 +127,6 @@ String formatSyncOffset(double offsetMs) {
/// Gets the duration package locale based on the current app locale.
/// Falls back to English if the locale is not supported by the duration package.
DurationLocale _getDurationLocale() {
// Get the current locale from slang's LocaleSettings
final appLocale = LocaleSettings.currentLocale;
final languageCode = appLocale.languageCode;
@@ -154,7 +136,6 @@ DurationLocale _getDurationLocale() {
try {
return DurationLocale.fromLanguageCode(languageCode) ?? const EnglishDurationLocale();
} catch (e) {
// Fallback to English if language code is not supported
return const EnglishDurationLocale();
}
}
@@ -188,7 +169,6 @@ String formatFinishTime(Duration remaining, {double rate = 1.0, required bool is
return formatClockTime(finishTime, is24Hour: is24Hour);
}
/// Takes a list of strings and returns one long string with each item in the list concatenated by a bullet
String toBulletedString(List<String> parts) {
return parts.join(' · ');
}
@@ -207,10 +187,8 @@ String formatPlaybackRate(double rate, {bool normalAtOne = false}) {
/// If there is any error, `dateString` is returned as is
String formatFullDate(String dateString) {
try {
// Parse the date
final date = DateTime.parse(dateString);
// Create a DateFormat with the full date pattern for the current locale
final formatter = DateFormat.yMMMMd(LocaleSettings.currentLocale.languageCode);
return formatter.format(date);
-1
View File
@@ -1,4 +1,3 @@
/// Builds a globalKey string from [serverId] and [ratingKey].
String buildGlobalKey(String serverId, String ratingKey) => '$serverId:$ratingKey';
/// Separator used by profile-owned rows whose public media identity is still
-3
View File
@@ -3,7 +3,6 @@ import '../services/settings_service.dart' show LibraryDensity;
import 'layout_constants.dart';
import 'platform_detector.dart';
/// Utility class for calculating consistent grid sizes across the app
class GridSizeCalculator {
static double _lerp(double min, double max, double t) => min + (max - min) * t;
@@ -68,12 +67,10 @@ class GridSizeCalculator {
return availableWidth / columns;
}
/// Check if the given index is in the first row of a grid with given column count.
static bool isFirstRow(int index, int columnCount) {
return index < columnCount;
}
/// Check if the given index is in the first column of a grid with given column count.
static bool isFirstColumn(int index, int columnCount) {
return index % columnCount == 0;
}
-2
View File
@@ -9,10 +9,8 @@ mixin HierarchicalEventMixin {
/// The id of the affected item (Plex ratingKey, Jellyfin GUID, …).
String get itemId;
/// Composite key: serverId:itemId.
String get globalKey;
/// Server this item belongs to.
String get serverId;
/// Parent chain for hierarchical matching.
-2
View File
@@ -19,7 +19,6 @@ void simulateKeyPress(LogicalKeyboardKey logicalKey) {
timeStamp: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch),
);
// Walk up the focus tree dispatching the key event
FocusNode? node = focusNode;
KeyEventResult result = KeyEventResult.ignored;
@@ -30,7 +29,6 @@ void simulateKeyPress(LogicalKeyboardKey logicalKey) {
node = node.parent;
}
// Send key up event
final keyUpEvent = KeyUpEvent(
physicalKey: physicalKey,
logicalKey: logicalKey,
-19
View File
@@ -3,42 +3,29 @@ import 'package:flutter/widgets.dart';
/// Layout and sizing constants used throughout the application
/// Screen width breakpoints for responsive design
class ScreenBreakpoints {
/// Breakpoint for mobile devices (< 600px)
static const double mobile = 600;
/// Breakpoint for wide tablets / small desktops (900px)
/// Used for intermediate responsive layouts
static const double wideTablet = 900;
/// Breakpoint for desktop devices (1200px)
static const double desktop = 1200;
/// Breakpoint for large desktop devices (1600px)
static const double largeDesktop = 1600;
// Legacy alias for backward compatibility
static const double tablet = mobile;
/// Whether width is mobile-sized (< 600px)
static bool isMobile(double width) => width < mobile;
/// Whether width is tablet-sized (600px - 1199px)
static bool isTablet(double width) => width >= mobile && width < desktop;
/// Whether width is wide tablet (900px - 1199px)
/// Useful for layouts that need more columns than phone but less than desktop
static bool isWideTablet(double width) => width >= wideTablet && width < desktop;
/// Whether width is desktop-sized (1200px - 1599px)
static bool isDesktop(double width) => width >= desktop && width < largeDesktop;
/// Whether width is large desktop-sized (>= 1600px)
static bool isLargeDesktop(double width) => width >= largeDesktop;
/// Whether width is desktop or larger (>= 1200px)
static bool isDesktopOrLarger(double width) => width >= desktop;
/// Whether width is wide tablet or larger (>= 900px)
static bool isWideTabletOrLarger(double width) => width >= wideTablet;
}
@@ -51,19 +38,13 @@ class AppDurations {
static const Duration snackBarLong = Duration(seconds: 4);
}
/// Grid layout constants
class GridLayoutConstants {
/// Default aspect ratio for media card grid cells (poster + text)
static const double posterAspectRatio = 2 / 3.3;
/// Aspect ratio for episode thumbnail image (16:9)
static const double episodeThumbnailAspectRatio = 16 / 9;
/// Aspect ratio for episode thumbnail grid cells (16:9 image + text area)
/// This is wider than posterAspectRatio but accounts for ~60px text below
static const double episodeGridCellAspectRatio = 1.4;
/// Grid spacing (edge-to-edge cards)
static const double crossAxisSpacing = 0;
static const double mainAxisSpacing = 0;
-3
View File
@@ -1,6 +1,5 @@
import 'base_notifier.dart';
/// Types of library refresh events
enum LibraryRefreshType { collections, playlists }
/// Notifier for triggering refreshes of library tabs.
@@ -20,12 +19,10 @@ class LibraryRefreshNotifier extends BaseNotifier<LibraryRefreshType> {
/// Stream for playlists tab (backward compatible)
Stream<void> get playlistsStream => stream.where((t) => t == LibraryRefreshType.playlists).cast<void>();
/// Notify that collections have changed
void notifyCollectionsChanged() {
notify(LibraryRefreshType.collections);
}
/// Notify that playlists have changed
void notifyPlaylistsChanged() {
notify(LibraryRefreshType.playlists);
}
+1 -6
View File
@@ -103,16 +103,13 @@ class LogRedactionManager {
/// Redact known sensitive values from the provided message.
static String redact(String message) {
// Pass 1: IPv4 addresses (regex pattern)
var redacted = message.replaceAllMapped(
_ipv4Pattern,
(match) => _maskIpv4(match.group(1)!, match.group(2)!, match.group(5)!),
);
// Pass 2: Strip X-Plex-Token query parameters (pattern-based, no pre-registration needed)
redacted = redacted.replaceAll(_plexTokenQueryParam, 'X-Plex-Token=[REDACTED]');
// Pass 2b: Strip Jellyfin api_key/Quick Connect query parameters and Emby/MediaBrowser headers.
redacted = redacted.replaceAll(_jellyfinApiKeyQueryParam, 'api_key=[REDACTED]');
redacted = redacted.replaceAll(_jellyfinQuickConnectSecretQueryParam, 'secret=[REDACTED]');
redacted = redacted.replaceAllMapped(_embyTokenHeader, (m) {
@@ -122,7 +119,6 @@ class LogRedactionManager {
});
redacted = redacted.replaceAll(_mediaBrowserTokenHeader, 'Token="[REDACTED]"');
// Pass 3: All tracked values in single pass
if (_combinedPattern != null) {
redacted = redacted.replaceAllMapped(_combinedPattern!, (match) {
final value = match.group(0)!;
@@ -155,9 +151,8 @@ class LogRedactionManager {
/// Add value to set with FIFO eviction if limit exceeded.
static void _addWithLimit(Set<String> set, String value, int maxSize) {
if (set.contains(value)) return; // Already tracked
if (set.contains(value)) return;
// Evict oldest entries if at capacity
while (set.length >= maxSize) {
set.remove(set.first);
}
+4 -20
View File
@@ -84,37 +84,26 @@ class MediaImageHelper {
switch (imageType) {
case ImageType.art:
// For art/background images, preserve aspect ratio while covering container
// Calculate dimensions that ensure the image covers the container without stretching
// This mimics BoxFit.cover behavior for the transcoding request
// Use larger dimensions to ensure coverage while preserving aspect ratio
// This will request a slightly larger image that can be cropped by Flutter's BoxFit.cover
final coverWidth = targetWidth * 1.1; // 10% larger for better coverage
final coverWidth = targetWidth * 1.1;
final coverHeight = targetHeight * 1.1;
return roundDimensions(coverWidth, coverHeight);
case ImageType.logo:
// For logos, use generous bounds to avoid forcing aspect ratio
// Prefer width-based scaling for most logos
final logoWidth = targetWidth;
final logoHeight = targetHeight; // Allow full height flexibility
final logoHeight = targetHeight;
return roundDimensions(logoWidth, logoHeight);
case ImageType.thumb:
// For episode thumbs, optimize for 16:9 but allow flexibility
final thumbHeight = targetHeight;
final thumbWidth = min(targetWidth, thumbHeight * (16 / 9));
return roundDimensions(thumbWidth, thumbHeight);
case ImageType.avatar:
// For avatars, use square dimensions based on smaller constraint
final size = min(targetWidth, targetHeight);
return roundDimensions(size, size);
case ImageType.poster:
// For posters, maintain 2:3 aspect ratio (width:height)
final calculatedWidth = min(targetWidth, targetHeight * (2 / 3));
final calculatedHeight = calculatedWidth * (3 / 2);
return roundDimensions(calculatedWidth, calculatedHeight);
@@ -138,8 +127,6 @@ class MediaImageHelper {
if (thumbPath == null || thumbPath.isEmpty) return '';
final basePath = thumbPath;
// External absolute URLs (EPG provider images, Jellyfin self-absolutized
// image URLs).
if (basePath.startsWith('http://') || basePath.startsWith('https://')) {
// Self-contained Jellyfin URLs already carry their own auth
// (`api_key=...`). Append `MaxWidth/MaxHeight` so we still get DPR
@@ -147,7 +134,6 @@ class MediaImageHelper {
// honours those query params.
if (basePath.contains('api_key=')) {
if (!enableTranscoding) return basePath;
// If size params are already attached, leave them.
if (basePath.contains('MaxWidth=') || basePath.contains('maxWidth=') || basePath.contains('Width=')) {
return basePath;
}
@@ -178,9 +164,8 @@ class MediaImageHelper {
// size-hint params (`/photo/:/transcode` for Plex, `MaxWidth/MaxHeight`
// for Jellyfin). The interface guarantees both honour width/height.
if (client == null) {
// Offline mode and a relative path the cached entry should already
// exist under whatever URL was originally fetched. Returning empty
// matches the pre-refactor behaviour.
// Offline + relative path: the cached entry already exists under the
// URL originally fetched, so returning '' matches pre-refactor behaviour.
return '';
}
@@ -240,7 +225,6 @@ class MediaImageHelper {
static bool shouldTranscode(String? imagePath) {
if (imagePath == null || imagePath.isEmpty) return false;
// Don't transcode already processed images or external URLs
if (imagePath.contains('/photo/:/transcode') ||
imagePath.startsWith('http://') ||
imagePath.startsWith('https://')) {
-3
View File
@@ -99,7 +99,6 @@ Future<MediaNavigationResult> navigateToMediaItem(
case MediaKind.clip:
case MediaKind.episode:
// For episodes and clips (trailers/extras), start playback directly
final result = await navigateToVideoPlayer(context, metadata: mi, isOffline: isOffline);
if (result == true) {
onRefresh?.call(mi.id);
@@ -108,7 +107,6 @@ Future<MediaNavigationResult> navigateToMediaItem(
case MediaKind.movie:
if (playDirectly) {
// For movies in continue watching, start playback directly
final result = await navigateToVideoPlayer(context, metadata: mi, isOffline: isOffline);
if (result == true) {
onRefresh?.call(mi.id);
@@ -118,7 +116,6 @@ Future<MediaNavigationResult> navigateToMediaItem(
return _showDetail(context, mi, isOffline, onRefresh);
case MediaKind.season:
// Navigate to the parent show with the season tab pre-selected
if (mi.parentId != null) {
final showStub = MediaItem(
id: mi.parentId!,
+3 -10
View File
@@ -50,7 +50,6 @@ void throwIfHttpError(MediaServerResponse r) {
class AbortController {
final _completer = Completer<void>();
/// The future that triggers abort when completed.
Future<void> get trigger => _completer.future;
bool get isAborted => _completer.isCompleted;
@@ -201,7 +200,6 @@ class MediaServerHttpClient {
final mergedHeaders = <String, String>{...defaultHeaders, ...?headers};
// Build the request — use AbortableRequest when abort is provided
final http.Request request;
if (abort != null) {
request = http.AbortableRequest(method, uri, abortTrigger: abort.trigger);
@@ -213,12 +211,10 @@ class MediaServerHttpClient {
final sw = Stopwatch()..start();
try {
// Phase 1: send + receive headers (connect timeout)
final streamed = await _client
.send(request)
.namedTimeout(timeout ?? connectTimeout, operation: '$method ${uri.path} connect');
// Phase 2: consume body (receive timeout)
final bytes = await streamed.stream.toBytes().namedTimeout(
timeout ?? receiveTimeout,
operation: '$method ${uri.path} receive',
@@ -320,13 +316,10 @@ class MediaServerHttpClient {
return;
}
// Map or List → JSON encode
request.body = jsonEncode(body);
// Only set content-type if the caller hasn't already. http.BaseRequest's
// headers map is case-sensitive, so we must check both common casings —
// Jellyfin returns 415 if a `Content-Type: application/json` from the
// default headers ends up coexisting with a lowercase `content-type:
// application/json; charset=utf-8` we'd append below.
// http.BaseRequest's headers map is case-sensitive; Jellyfin returns 415
// if both `Content-Type` (from defaults) and `content-type` (added below)
// end up coexisting, so check both casings before adding.
final hasContentType = request.headers.keys.any((k) => k.toLowerCase() == 'content-type');
if (!hasContentType) {
request.headers['content-type'] = 'application/json';
-8
View File
@@ -3,18 +3,14 @@
/// timeouts are kept here so the budgets per phase are visible at a
/// glance.
class MediaServerTimeouts {
// ── Per-server HTTP request budgets (apply to both backends) ───
/// HTTP connect timeout for individual HTTP requests to a media server.
static const connect = Duration(seconds: 10);
/// HTTP receive timeout for streaming/large responses from a media server.
static const receive = Duration(seconds: 120);
/// Retry budget for home `/hubs` startup calls. These endpoints can be slow
/// while Plex wakes idle disks, but should not block forever.
static const homeHubAttemptTimeouts = [Duration(seconds: 10), Duration(seconds: 5), Duration(milliseconds: 2500)];
// ── Plex server discovery / endpoint racing ────────────────────
/// Timeout for probing a cached/preferred endpoint before falling back to
/// the full candidate race (used in [PlexServer.findBestWorkingConnection]).
static const preferredEndpointProbe = Duration(milliseconds: 1500);
@@ -31,14 +27,10 @@ class MediaServerTimeouts {
/// default 10s connect budget is too tight on Fire-TV cold starts.
static const tune = Duration(seconds: 30);
// ── plex.tv (auth provider) ─────────────────────────────────────
/// HTTP connect timeout for plex.tv / clients.plex.tv API requests.
static const plexTvConnect = Duration(seconds: 5);
/// HTTP receive timeout for plex.tv / clients.plex.tv API responses.
static const plexTvReceive = Duration(seconds: 10);
// ── Jellyfin auth flow ──────────────────────────────────────────
/// Probe + token-validate timeout — Jellyfin servers respond fast on
/// `/System/Info/Public` and `/Users/Me`.
static const jellyfinProbe = Duration(seconds: 8);
-2
View File
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'platform_detector.dart';
/// Helper class for managing device orientation preferences across the app.
class OrientationHelper {
/// Restores default orientation preferences based on device type.
///
@@ -17,7 +16,6 @@ class OrientationHelper {
if (isPhone) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
} else {
// For tablets and desktop, allow all orientations
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
-8
View File
@@ -76,16 +76,11 @@ class TvDetectionService {
static void setForceTVSync(bool value) => _instance?.setForceTv(value);
}
/// Utility class for platform detection
class PlatformDetector {
/// Detects if running on a TV platform (Android TV or Apple TV).
/// Requires TvDetectionService to be initialized.
static bool isTV() {
return TvDetectionService.isTVSync();
}
/// Detects if running specifically on Apple TV (tvOS).
/// Requires TvDetectionService to be initialized.
static bool isAppleTV() {
return TvDetectionService.isAppleTVSync();
}
@@ -111,7 +106,6 @@ class PlatformDetector {
return platform == TargetPlatform.iOS || platform == TargetPlatform.android;
}
/// Detects if running on a handheld mobile device (phone or tablet, not TV).
static bool isHandheld(BuildContext context) {
return isMobile(context) && !isTV();
}
@@ -138,11 +132,9 @@ class PlatformDetector {
// Convert diagonal from logical pixels to inches (assuming 160 DPI as baseline)
final diagonalInches = diagonal / (devicePixelRatio * 160 / 2.54);
// Consider devices with diagonal >= 7 inches as tablets
return diagonalInches >= 7.0;
}
/// Detects if the device is a phone (mobile but not tablet)
static bool isPhone(BuildContext context) {
return isHandheld(context) && !isTablet(context);
}
-7
View File
@@ -4,24 +4,17 @@
class PlexCacheParser {
PlexCacheParser._();
/// Extract the Metadata list from a cached response
///
/// Returns null if MediaContainer or Metadata is not present
static List<dynamic>? extractMetadataList(Map<String, dynamic>? cached) {
if (cached == null) return null;
return cached['MediaContainer']?['Metadata'] as List?;
}
/// Extract the first metadata item from a cached response
///
/// Returns null if no metadata exists
static Map<String, dynamic>? extractFirstMetadata(Map<String, dynamic>? cached) {
final list = extractMetadataList(cached);
if (list == null || list.isEmpty) return null;
return list.first as Map<String, dynamic>;
}
/// Extract Chapter list from the first metadata item
static List<dynamic>? extractChapters(Map<String, dynamic>? cached) {
final metadata = extractFirstMetadata(cached);
if (metadata == null) return null;
-10
View File
@@ -16,7 +16,6 @@ extension ProviderExtensions on BuildContext {
HiddenLibrariesProvider get hiddenLibraries => Provider.of<HiddenLibrariesProvider>(this, listen: false);
// Direct profile settings access (nullable)
MediaServerUserProfile? get profileSettings => userProfile.profileSettings;
/// Internal: resolve a [PlexClient] from a serverId or fall back to the
@@ -50,22 +49,16 @@ extension ProviderExtensions on BuildContext {
return client;
}
/// Get PlexClient for a specific server ID. Throws if unavailable.
PlexClient getPlexClientForServer(String serverId) => _requireClient(serverId, fallback: false);
/// Get PlexClient for a specific server ID, or null if unavailable.
PlexClient? tryGetPlexClientForServer(String? serverId) {
if (serverId == null) return null;
final provider = Provider.of<MultiServerProvider>(this, listen: false);
return provider.getPlexClientForServer(serverId);
}
/// Get PlexClient for a library, falling back to the first online server
/// when the library has no serverId. Throws if no client is available.
PlexClient getPlexClientForLibrary(MediaLibrary library) => _requireClient(library.serverId);
/// Get client for a serverId, falling back to the first online server.
/// Throws if no client is available.
PlexClient getPlexClientWithFallback(String? serverId) => _requireClient(serverId);
// ── Backend-neutral helpers ──────────────────────────────────────
@@ -80,7 +73,6 @@ extension ProviderExtensions on BuildContext {
return _resolvePrioritized(serverId, provider.onlineServerIds, provider.getClientForServer);
}
/// Get a [MediaServerClient] for the given serverId, or null.
MediaServerClient? tryGetMediaClientForServer(String? serverId) {
if (serverId == null) return null;
final provider = Provider.of<MultiServerProvider>(this, listen: false);
@@ -96,8 +88,6 @@ extension ProviderExtensions on BuildContext {
return c;
}
/// Get a [MediaServerClient] for [library], falling back to the first
/// online server when the library has no serverId. Throws if none.
MediaServerClient getMediaClientForLibrary(MediaLibrary library) {
final c = _resolveMediaClient(library.serverId);
if (c == null) throw Exception(t.errors.noClientAvailable);
-3
View File
@@ -16,7 +16,6 @@ class RatingInfo {
RatingInfo? parseRatingImage(String? imageUri, double? value) {
if (imageUri == null || value == null) return null;
// Rotten Tomatoes critic ratings
if (imageUri.startsWith('rottentomatoes://image.rating.')) {
final suffix = imageUri.substring('rottentomatoes://image.rating.'.length);
final percent = '${(value * 10).toStringAsFixed(0)}%';
@@ -29,12 +28,10 @@ RatingInfo? parseRatingImage(String? imageUri, double? value) {
};
}
// IMDb
if (imageUri.startsWith('imdb://')) {
return RatingInfo('assets/rating_icons/imdb.svg', value.toStringAsFixed(1));
}
// TMDB
if (imageUri.startsWith('themoviedb://')) {
return RatingInfo('assets/rating_icons/tmdb.svg', '${(value * 10).toStringAsFixed(0)}%');
}
-3
View File
@@ -16,7 +16,6 @@ class SmartDeletionHandler {
bool dialogShown = false;
bool deletionComplete = false;
// Start a timer to show dialog after delay
Future.delayed(Duration(milliseconds: delayMs), () {
if (!deletionComplete && context.mounted) {
dialogShown = true;
@@ -35,7 +34,6 @@ class SmartDeletionHandler {
}
}
/// Show progress dialog and listen to updates
static void _showProgressDialog(BuildContext context, DownloadProvider _, String globalKey) {
showDialog(
context: context,
@@ -44,7 +42,6 @@ class SmartDeletionHandler {
builder: (context, provider, child) {
final progress = provider.getDeletionProgress(globalKey);
// If no progress, show simple fallback
if (progress == null) {
return AlertDialog(
content: Row(
+1 -29
View File
@@ -10,25 +10,10 @@ final rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final mainScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// Types of snackbars available in the app
enum SnackBarType {
/// Standard informational snackbar
info,
/// Success snackbar (green background)
success,
/// Error snackbar (red background)
error,
}
enum SnackBarType { info, success, error }
/// Utility functions for showing snackbars throughout the application
/// Shows a snackbar with the specified type
///
/// [context] The build context
/// [message] The message to display
/// [type] The type of snackbar (info, success, error)
/// [duration] Optional duration override
void showSnackBar(BuildContext context, String message, {SnackBarType type = SnackBarType.info, Duration? duration}) {
if (!context.mounted) return;
@@ -43,19 +28,10 @@ void showSnackBar(BuildContext context, String message, {SnackBarType type = Sna
);
}
/// Shows a standard snackbar with a message
///
/// [context] The build context
/// [message] The message to display
/// [duration] Optional duration, defaults to 3 seconds
void showAppSnackBar(BuildContext context, String message, {Duration? duration}) {
showSnackBar(context, message, type: SnackBarType.info, duration: duration);
}
/// Shows an error snackbar with a message
///
/// [context] The build context
/// [message] The error message to display
void showErrorSnackBar(BuildContext context, String message) {
showSnackBar(context, message, type: SnackBarType.error);
}
@@ -77,10 +53,6 @@ void showMainSnackBar(String message, {Duration duration = AppDurations.snackBar
..showSnackBar(SnackBar(content: Text(message), duration: duration));
}
/// Shows a success snackbar with a message
///
/// [context] The build context
/// [message] The success message to display
void showSuccessSnackBar(BuildContext context, String message) {
showSnackBar(context, message, type: SnackBarType.success);
}
-7
View File
@@ -79,13 +79,9 @@ Set<String> _subtitleCodecAliases(String? codec) {
String _metadataToken(String value) => value.trim().toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]+'), '_');
/// Utility for building track labels for audio and subtitle tracks.
class TrackLabelBuilder {
TrackLabelBuilder._();
/// Build a label for an audio track.
///
/// Combines title, language, codec, and channel count.
static String buildAudioLabel({
String? title,
String? language,
@@ -109,9 +105,6 @@ class TrackLabelBuilder {
);
}
/// Build a label for a subtitle track.
///
/// Combines title, language, and codec (with friendly codec names).
static String buildSubtitleLabel({
String? title,
String? language,
+1 -8
View File
@@ -55,7 +55,6 @@ Future<bool?> navigateToVideoPlayer(
bool usePushReplacement = false,
bool isOffline = false,
}) async {
// Extract context-dependent values before any async operations
final navigator = Navigator.of(context);
final downloadProvider = context.read<DownloadProvider>();
// Use the manager-routed lookup so Jellyfin items don't trip the
@@ -63,7 +62,6 @@ Future<bool?> navigateToVideoPlayer(
final manager = context.read<MultiServerProvider>().serverManager;
final mediaClient = isOffline ? null : manager.getClient(metadata.serverId ?? '');
// Load saved media version preference if not explicitly provided
int mediaIndex = selectedMediaIndex ?? 0;
if (selectedMediaIndex == null) {
try {
@@ -73,9 +71,7 @@ Future<bool?> navigateToVideoPlayer(
if (savedPreference != null) {
mediaIndex = savedPreference;
}
} catch (e) {
// Ignore errors loading preference, use default
}
} catch (_) {}
}
// Check if external player is enabled
@@ -85,7 +81,6 @@ Future<bool?> navigateToVideoPlayer(
bool launched = false;
if (isOffline) {
// Offline mode: resolve local file path for the external player
final globalKey = metadata.globalKey;
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
if (videoPath != null && context.mounted) {
@@ -102,7 +97,6 @@ Future<bool?> navigateToVideoPlayer(
}
if (launched) return null;
// Fall through to built-in player on failure
}
} catch (e) {
appLogger.w('External player launch failed, falling back to built-in player', error: e);
@@ -174,7 +168,6 @@ Future<bool?> navigateToVideoPlayerWithRefresh(
appLogger.d('Returned from playback, refreshing metadata');
// Refresh data when returning from video player (skip if offline)
if (!isOffline && onRefresh != null) {
onRefresh();
}
-3
View File
@@ -4,7 +4,6 @@ import 'base_notifier.dart';
import 'global_key_utils.dart';
import 'hierarchical_event_mixin.dart';
/// Types of watch state changes
enum WatchStateChangeType { watched, unwatched, progressUpdate }
/// Event representing a watch state change with parent chain for hierarchical invalidation
@@ -82,10 +81,8 @@ class WatchStateNotifier extends BaseNotifier<WatchStateEvent> {
WatchStateNotifier._internal();
/// Filter for events affecting a specific server
Stream<WatchStateEvent> forServer(String serverId) => stream.where((e) => e.serverId == serverId);
/// Filter for events affecting a specific item or its children
Stream<WatchStateEvent> forItem(String itemId) => stream.where((e) => e.affectsItem(itemId));
/// Emit a watch state event with logging