Files
plezy/lib/main.dart
T

321 lines
10 KiB
Dart
Raw Normal View History

2025-10-18 00:05:20 +02:00
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:media_kit/media_kit.dart';
import 'package:window_manager/window_manager.dart';
2025-10-30 18:49:59 +01:00
import 'package:provider/provider.dart';
2025-11-04 16:23:45 +01:00
import 'package:url_launcher/url_launcher.dart';
2025-10-18 00:05:20 +02:00
import 'screens/main_screen.dart';
import 'screens/auth_screen.dart';
import 'services/storage_service.dart';
import 'services/plex_auth_service.dart';
2025-10-28 05:52:58 +01:00
import 'services/server_connection_service.dart';
2025-10-18 00:05:20 +02:00
import 'services/macos_titlebar_service.dart';
import 'services/fullscreen_state_manager.dart';
2025-11-04 16:23:45 +01:00
import 'services/update_service.dart';
2025-11-05 08:19:50 +01:00
import 'services/settings_service.dart';
2025-10-30 18:49:59 +01:00
import 'providers/user_profile_provider.dart';
import 'providers/plex_client_provider.dart';
2025-10-31 00:58:39 +01:00
import 'providers/theme_provider.dart';
2025-11-04 04:17:39 +01:00
import 'providers/settings_provider.dart';
2025-11-04 06:45:16 +01:00
import 'providers/hidden_libraries_provider.dart';
2025-11-05 07:28:23 +01:00
import 'providers/playback_state_provider.dart';
2025-10-18 00:05:20 +02:00
import 'utils/language_codes.dart';
import 'utils/app_logger.dart';
2025-10-30 18:49:59 +01:00
import 'utils/provider_extensions.dart';
import 'utils/orientation_helper.dart';
2025-11-10 10:02:34 +01:00
import 'i18n/strings.g.dart';
2025-10-18 00:05:20 +02:00
void main() async {
WidgetsFlutterBinding.ensureInitialized();
2025-11-10 10:02:34 +01:00
// Initialize settings first to get saved locale
final settings = await SettingsService.getInstance();
final savedLocale = settings.getAppLocale();
// Initialize localization with saved locale
LocaleSettings.setLocale(savedLocale);
2025-11-04 04:17:39 +01:00
// Configure image cache for large libraries
PaintingBinding.instance.imageCache.maximumSizeBytes = 500 << 20; // 500MB
PaintingBinding.instance.imageCache.maximumSize = 500; // 500 images
2025-10-18 00:05:20 +02:00
// Initialize window_manager for desktop platforms
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await windowManager.ensureInitialized();
}
// Configure macOS window with custom titlebar
await MacOSTitlebarService.setupCustomTitlebar();
// Initialize MediaKit
MediaKit.ensureInitialized();
2025-11-01 05:50:24 +01:00
// Note: Orientation will be set dynamically based on device type in MainApp
2025-10-18 00:05:20 +02:00
await StorageService.getInstance();
// Initialize language codes for track selection
await LanguageCodes.initialize();
2025-11-05 08:19:50 +01:00
// Initialize logger level based on debug setting
final debugEnabled = settings.getEnableDebugLogging();
setLoggerLevel(debugEnabled);
2025-10-18 00:05:20 +02:00
// Start global fullscreen state monitoring
FullscreenStateManager().startMonitoring();
// DTD service is available for MCP tooling connection if needed
runApp(const MainApp());
}
// Global RouteObserver for tracking navigation
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
2025-10-30 18:49:59 +01:00
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => PlexClientProvider()),
ChangeNotifierProvider(
create: (context) => UserProfileProvider()..initialize(),
),
2025-10-31 00:58:39 +01:00
ChangeNotifierProvider(create: (context) => ThemeProvider()),
2025-11-04 04:17:39 +01:00
ChangeNotifierProvider(create: (context) => SettingsProvider()),
2025-11-04 06:45:16 +01:00
ChangeNotifierProvider(create: (context) => HiddenLibrariesProvider()),
2025-11-05 07:28:23 +01:00
ChangeNotifierProvider(create: (context) => PlaybackStateProvider()),
2025-10-30 18:49:59 +01:00
],
2025-10-31 00:58:39 +01:00
child: Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
2025-11-10 10:02:34 +01:00
return TranslationProvider(
child: MaterialApp(
title: t.app.title,
debugShowCheckedModeBanner: false,
theme: themeProvider.lightTheme,
darkTheme: themeProvider.darkTheme,
themeMode: themeProvider.materialThemeMode,
navigatorObservers: [routeObserver],
home: const OrientationAwareSetup(),
),
2025-10-31 00:58:39 +01:00
);
},
2025-10-30 18:49:59 +01:00
),
2025-10-18 00:05:20 +02:00
);
}
}
2025-11-01 05:50:24 +01:00
class OrientationAwareSetup extends StatefulWidget {
const OrientationAwareSetup({super.key});
@override
State<OrientationAwareSetup> createState() => _OrientationAwareSetupState();
}
class _OrientationAwareSetupState extends State<OrientationAwareSetup> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_setOrientationPreferences();
}
void _setOrientationPreferences() {
OrientationHelper.restoreDefaultOrientations(context);
2025-11-01 05:50:24 +01:00
}
@override
Widget build(BuildContext context) {
return const SetupScreen();
}
}
2025-10-18 00:05:20 +02:00
class SetupScreen extends StatefulWidget {
const SetupScreen({super.key});
@override
State<SetupScreen> createState() => _SetupScreenState();
}
class _SetupScreenState extends State<SetupScreen> {
@override
void initState() {
super.initState();
_loadSavedCredentials();
}
2025-11-04 16:23:45 +01:00
void _checkForUpdatesOnStartup() async {
// Delay slightly to allow UI to settle
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
try {
final updateInfo = await UpdateService.checkForUpdatesOnStartup();
if (updateInfo != null && updateInfo['hasUpdate'] == true && mounted) {
_showUpdateDialog(updateInfo);
}
} catch (e) {
appLogger.e('Error checking for updates', error: e);
}
}
void _showUpdateDialog(Map<String, dynamic> updateInfo) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
2025-11-10 10:02:34 +01:00
title: Text(t.update.available),
2025-11-04 16:23:45 +01:00
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
2025-11-10 10:02:34 +01:00
t.update.versionAvailable(version: updateInfo['latestVersion']),
2025-11-04 16:23:45 +01:00
style: Theme.of(dialogContext).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
2025-11-10 10:02:34 +01:00
t.update.currentVersion(version: updateInfo['currentVersion']),
2025-11-04 16:23:45 +01:00
style: Theme.of(dialogContext).textTheme.bodySmall,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
},
2025-11-10 10:02:34 +01:00
child: Text(t.common.later),
2025-11-04 16:23:45 +01:00
),
TextButton(
onPressed: () async {
await UpdateService.skipVersion(updateInfo['latestVersion']);
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
},
2025-11-10 10:02:34 +01:00
child: Text(t.update.skipVersion),
2025-11-04 16:23:45 +01:00
),
FilledButton(
onPressed: () async {
final url = Uri.parse(updateInfo['releaseUrl']);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
},
2025-11-10 10:02:34 +01:00
child: Text(t.update.viewRelease),
2025-11-04 16:23:45 +01:00
),
],
);
},
);
}
2025-10-18 00:05:20 +02:00
Future<void> _loadSavedCredentials() async {
final storage = await StorageService.getInstance();
// Check if we have server data
final serverData = storage.getServerData();
final clientId = storage.getClientIdentifier();
final plexToken = storage.getPlexToken();
2025-10-30 18:49:59 +01:00
// Get current user's server token (prioritize over original server token)
final currentUserToken = storage.getToken();
2025-10-18 00:05:20 +02:00
if (serverData != null && clientId != null) {
try {
// Recreate PlexServer from stored data
final server = PlexServer.fromJson(serverData);
2025-10-30 18:49:59 +01:00
// Use current user's token if available, fallback to server's original token
final tokenToUse = currentUserToken ?? server.accessToken;
appLogger.d(
'App startup token selection: currentUserToken=${currentUserToken != null ? 'present' : 'null'}, using=${currentUserToken != null ? 'current user' : 'original server'} token',
);
// Create updated server with correct token for current user
final serverWithCurrentToken = PlexServer(
name: server.name,
clientIdentifier: server.clientIdentifier,
accessToken: tokenToUse,
connections: server.connections,
owned: server.owned,
product: server.product,
platform: server.platform,
lastSeenAt: server.lastSeenAt,
presence: server.presence,
);
// Connect using the optimized service with current user's token
2025-10-28 05:52:58 +01:00
final result = await ServerConnectionService.connectToServer(
2025-10-30 18:49:59 +01:00
serverWithCurrentToken,
2025-10-28 05:52:58 +01:00
clientIdentifier: clientId,
verifyServer: true,
fetchUserProfile: plexToken != null,
plexToken: plexToken,
);
// Handle result
if (result.isSuccess) {
2025-11-04 16:23:45 +01:00
// Success! Set client in provider
2025-10-28 05:52:58 +01:00
if (mounted) {
2025-10-30 18:49:59 +01:00
context.plexClient.setClient(result.client!);
2025-11-04 16:23:45 +01:00
// Check for updates BEFORE navigation to keep context valid
_checkForUpdatesOnStartup();
// Navigate to main screen after update check is initiated
2025-11-01 12:11:56 +01:00
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MainScreen(client: result.client!),
2025-10-18 00:05:20 +02:00
),
2025-11-01 12:11:56 +01:00
);
}
2025-10-18 00:05:20 +02:00
}
2025-10-28 05:52:58 +01:00
return;
2025-10-18 00:05:20 +02:00
} else {
2025-10-28 05:52:58 +01:00
// Connection failed, clear credentials
2025-10-18 00:05:20 +02:00
await storage.clearCredentials();
}
} catch (e) {
// Error loading or testing server
2025-10-28 05:52:58 +01:00
appLogger.e('Error during auto-login', error: e);
2025-10-18 00:05:20 +02:00
await storage.clearCredentials();
}
}
// No saved credentials or auto-login failed - show auth screen
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AuthScreen()),
);
}
}
@override
Widget build(BuildContext context) {
2025-11-10 10:02:34 +01:00
return Scaffold(
2025-10-18 00:05:20 +02:00
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
2025-11-10 10:02:34 +01:00
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(t.app.loading),
2025-10-18 00:05:20 +02:00
],
),
),
);
}
}