feat: settings page
This commit is contained in:
+14
-8
@@ -13,10 +13,10 @@ import 'services/macos_titlebar_service.dart';
|
||||
import 'services/fullscreen_state_manager.dart';
|
||||
import 'providers/user_profile_provider.dart';
|
||||
import 'providers/plex_client_provider.dart';
|
||||
import 'providers/theme_provider.dart';
|
||||
import 'utils/language_codes.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
import 'utils/provider_extensions.dart';
|
||||
import 'theme/mono_theme.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -65,14 +65,20 @@ class MainApp extends StatelessWidget {
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => UserProfileProvider()..initialize(),
|
||||
),
|
||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Plezy',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: monoTheme(dark: false),
|
||||
darkTheme: monoTheme(dark: true),
|
||||
navigatorObservers: [routeObserver],
|
||||
home: const SetupScreen(),
|
||||
child: Consumer<ThemeProvider>(
|
||||
builder: (context, themeProvider, child) {
|
||||
return MaterialApp(
|
||||
title: 'Plezy',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: themeProvider.lightTheme,
|
||||
darkTheme: themeProvider.darkTheme,
|
||||
themeMode: themeProvider.materialThemeMode,
|
||||
navigatorObservers: [routeObserver],
|
||||
home: const SetupScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/settings_service.dart' as settings;
|
||||
import '../theme/mono_theme.dart';
|
||||
|
||||
class ThemeProvider extends ChangeNotifier {
|
||||
late settings.SettingsService _settingsService;
|
||||
settings.ThemeMode _themeMode = settings.ThemeMode.system;
|
||||
late Brightness _systemBrightness;
|
||||
|
||||
ThemeProvider() {
|
||||
_systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
||||
_initializeSettings();
|
||||
|
||||
// Listen to system theme changes
|
||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () {
|
||||
_systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
||||
if (_themeMode == settings.ThemeMode.system) {
|
||||
notifyListeners();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _initializeSettings() async {
|
||||
_settingsService = await settings.SettingsService.getInstance();
|
||||
_themeMode = _settingsService.getThemeMode();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
settings.ThemeMode get themeMode => _themeMode;
|
||||
|
||||
ThemeData get lightTheme => monoTheme(dark: false);
|
||||
ThemeData get darkTheme => monoTheme(dark: true);
|
||||
|
||||
ThemeMode get materialThemeMode {
|
||||
switch (_themeMode) {
|
||||
case settings.ThemeMode.light:
|
||||
return ThemeMode.light;
|
||||
case settings.ThemeMode.dark:
|
||||
return ThemeMode.dark;
|
||||
case settings.ThemeMode.system:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isDarkMode {
|
||||
switch (_themeMode) {
|
||||
case settings.ThemeMode.light:
|
||||
return false;
|
||||
case settings.ThemeMode.dark:
|
||||
return true;
|
||||
case settings.ThemeMode.system:
|
||||
return _systemBrightness == Brightness.dark;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(settings.ThemeMode mode) async {
|
||||
if (_themeMode != mode) {
|
||||
_themeMode = mode;
|
||||
await _settingsService.setThemeMode(mode);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String get themeModeDisplayName {
|
||||
switch (_themeMode) {
|
||||
case settings.ThemeMode.light:
|
||||
return 'Light';
|
||||
case settings.ThemeMode.dark:
|
||||
return 'Dark';
|
||||
case settings.ThemeMode.system:
|
||||
return 'System';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get themeModeIcon {
|
||||
switch (_themeMode) {
|
||||
case settings.ThemeMode.light:
|
||||
return Icons.light_mode;
|
||||
case settings.ThemeMode.dark:
|
||||
return Icons.dark_mode;
|
||||
case settings.ThemeMode.system:
|
||||
return Icons.brightness_auto;
|
||||
}
|
||||
}
|
||||
|
||||
void toggleTheme() {
|
||||
switch (_themeMode) {
|
||||
case settings.ThemeMode.system:
|
||||
setThemeMode(settings.ThemeMode.light);
|
||||
break;
|
||||
case settings.ThemeMode.light:
|
||||
setThemeMode(settings.ThemeMode.dark);
|
||||
break;
|
||||
case settings.ThemeMode.dark:
|
||||
setThemeMode(settings.ThemeMode.system);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import 'licenses_screen.dart';
|
||||
|
||||
class AboutScreen extends StatefulWidget {
|
||||
const AboutScreen({super.key});
|
||||
@@ -81,61 +82,16 @@ class _AboutScreenState extends State<AboutScreen> {
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
showLicensePage(
|
||||
context: context,
|
||||
applicationName: appName,
|
||||
applicationVersion: appVersion,
|
||||
applicationIcon: Image.asset(
|
||||
'assets/plezy.png',
|
||||
width: 48,
|
||||
height: 48,
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LicensesScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Key Dependencies
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Key Dependencies',
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDependencyItem('http', 'HTTP networking'),
|
||||
_buildDependencyItem('dio', 'Advanced HTTP client'),
|
||||
_buildDependencyItem(
|
||||
'cached_network_image',
|
||||
'Image caching',
|
||||
),
|
||||
_buildDependencyItem('media_kit', 'Video playback'),
|
||||
_buildDependencyItem(
|
||||
'shared_preferences',
|
||||
'Local storage',
|
||||
),
|
||||
_buildDependencyItem('xml', 'XML parsing'),
|
||||
_buildDependencyItem('url_launcher', 'External links'),
|
||||
_buildDependencyItem(
|
||||
'window_manager',
|
||||
'Desktop window management',
|
||||
),
|
||||
_buildDependencyItem(
|
||||
'macos_window_utils',
|
||||
'macOS window controls',
|
||||
),
|
||||
_buildDependencyItem('logger', 'Logging'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
@@ -146,32 +102,4 @@ class _AboutScreenState extends State<AboutScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDependencyItem(String name, String description) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.circle, size: 6),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(
|
||||
text: ' - $description',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
style: const TextStyle(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
|
||||
class MergedLicenseEntry {
|
||||
final String packageName;
|
||||
final List<LicenseEntry> licenseEntries;
|
||||
final Set<String> allPackageNames;
|
||||
|
||||
MergedLicenseEntry({
|
||||
required this.packageName,
|
||||
required this.licenseEntries,
|
||||
required this.allPackageNames,
|
||||
});
|
||||
}
|
||||
|
||||
class LicensesScreen extends StatefulWidget {
|
||||
const LicensesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LicensesScreen> createState() => _LicensesScreenState();
|
||||
}
|
||||
|
||||
class _LicensesScreenState extends State<LicensesScreen> {
|
||||
List<MergedLicenseEntry> _mergedLicenses = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLicenses();
|
||||
}
|
||||
|
||||
Future<void> _loadLicenses() async {
|
||||
final licenseMap = <String, List<LicenseEntry>>{};
|
||||
final allPackageNames = <String, Set<String>>{};
|
||||
|
||||
await for (final license in LicenseRegistry.licenses) {
|
||||
for (final packageName in license.packages) {
|
||||
if (!licenseMap.containsKey(packageName)) {
|
||||
licenseMap[packageName] = [];
|
||||
allPackageNames[packageName] = <String>{};
|
||||
}
|
||||
licenseMap[packageName]!.add(license);
|
||||
allPackageNames[packageName]!.addAll(license.packages);
|
||||
}
|
||||
}
|
||||
|
||||
final mergedLicenses = licenseMap.entries.map((entry) {
|
||||
return MergedLicenseEntry(
|
||||
packageName: entry.key,
|
||||
licenseEntries: entry.value,
|
||||
allPackageNames: allPackageNames[entry.key]!,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
mergedLicenses.sort((a, b) => a.packageName.compareTo(b.packageName));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_mergedLicenses = mergedLicenses;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const CustomAppBar(title: Text('Licenses'), pinned: true),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final mergedLicense = _mergedLicenses[index];
|
||||
final packageName = mergedLicense.packageName;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
packageName,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: mergedLicense.licenseEntries.length > 1
|
||||
? Text('${mergedLicense.licenseEntries.length} licenses')
|
||||
: null,
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showLicenseDetail(mergedLicense),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: _mergedLicenses.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLicenseDetail(MergedLicenseEntry mergedLicense) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _LicenseDetailScreen(
|
||||
mergedLicense: mergedLicense,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LicenseDetailScreen extends StatelessWidget {
|
||||
final MergedLicenseEntry mergedLicense;
|
||||
|
||||
const _LicenseDetailScreen({
|
||||
required this.mergedLicense,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final packageName = mergedLicense.packageName;
|
||||
final licenseEntries = mergedLicense.licenseEntries;
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
title: Text(packageName),
|
||||
pinned: true,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Package info card
|
||||
if (mergedLicense.allPackageNames.length > 1)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Related Packages',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
mergedLicense.allPackageNames.join(', '),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (mergedLicense.allPackageNames.length > 1)
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// License cards
|
||||
...licenseEntries.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final license = entry.value;
|
||||
final isMultipleLicenses = licenseEntries.length > 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isMultipleLicenses ? 'License ${index + 1}' : 'License',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...license.paragraphs.map((paragraph) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: SelectableText(
|
||||
paragraph.text,
|
||||
style: TextStyle(
|
||||
fontFamily: paragraph.indent > 0 ? 'monospace' : null,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (index < licenseEntries.length - 1)
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import '../mixins/refreshable.dart';
|
||||
import 'discover_screen.dart';
|
||||
import 'libraries_screen.dart';
|
||||
import 'search_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class MainScreen extends StatefulWidget {
|
||||
final PlexClient client;
|
||||
@@ -26,6 +27,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware {
|
||||
final GlobalKey<State<DiscoverScreen>> _discoverKey = GlobalKey();
|
||||
final GlobalKey<State<LibrariesScreen>> _librariesKey = GlobalKey();
|
||||
final GlobalKey<State<SearchScreen>> _searchKey = GlobalKey();
|
||||
final GlobalKey<State<SettingsScreen>> _settingsKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -39,6 +41,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware {
|
||||
),
|
||||
LibrariesScreen(key: _librariesKey, userProfile: widget.userProfile),
|
||||
SearchScreen(key: _searchKey, userProfile: widget.userProfile),
|
||||
SettingsScreen(key: _settingsKey),
|
||||
];
|
||||
|
||||
// Set up data invalidation callback for profile switching
|
||||
@@ -141,6 +144,11 @@ class _MainScreenState extends State<MainScreen> with RouteAware {
|
||||
selectedIcon: Icon(Icons.search),
|
||||
label: 'Search',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import '../providers/theme_provider.dart';
|
||||
import '../services/settings_service.dart' as settings;
|
||||
import '../services/keyboard_shortcuts_service.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/hotkey_recorder_widget.dart';
|
||||
import 'about_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
late settings.SettingsService _settingsService;
|
||||
late KeyboardShortcutsService _keyboardService;
|
||||
bool _isLoading = true;
|
||||
|
||||
bool _enableDebugLogging = false;
|
||||
bool _enableHardwareDecoding = true;
|
||||
int _videoBufferSize = 64;
|
||||
int _audioBufferSize = 8;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
_settingsService = await settings.SettingsService.getInstance();
|
||||
_keyboardService = await KeyboardShortcutsService.getInstance();
|
||||
|
||||
setState(() {
|
||||
_enableDebugLogging = _settingsService.getEnableDebugLogging();
|
||||
_enableHardwareDecoding = _settingsService.getEnableHardwareDecoding();
|
||||
_videoBufferSize = _settingsService.getVideoBufferSize();
|
||||
_audioBufferSize = _settingsService.getAudioBufferSize();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const CustomAppBar(title: Text('Settings'), pinned: true),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
_buildAppearanceSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildVideoPlaybackSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildKeyboardShortcutsSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildAdvancedSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildAboutSection(),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppearanceSection() {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Appearance',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Consumer<ThemeProvider>(
|
||||
builder: (context, themeProvider, child) {
|
||||
return ListTile(
|
||||
leading: Icon(themeProvider.themeModeIcon),
|
||||
title: const Text('Theme'),
|
||||
subtitle: Text(themeProvider.themeModeDisplayName),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showThemeDialog(themeProvider),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoPlaybackSection() {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Video Playback',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.hardware),
|
||||
title: const Text('Hardware Decoding'),
|
||||
subtitle: const Text('Use hardware acceleration when available'),
|
||||
value: _enableHardwareDecoding,
|
||||
onChanged: (value) async {
|
||||
setState(() {
|
||||
_enableHardwareDecoding = value;
|
||||
});
|
||||
await _settingsService.setEnableHardwareDecoding(value);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: const Text('Video Buffer Size'),
|
||||
subtitle: Text('${_videoBufferSize}MB'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showBufferSizeDialog(true),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.audiotrack),
|
||||
title: const Text('Audio Buffer Size'),
|
||||
subtitle: Text('${_audioBufferSize}MB'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showBufferSizeDialog(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeyboardShortcutsSection() {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Keyboard Shortcuts',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.keyboard),
|
||||
title: const Text('Video Player Controls'),
|
||||
subtitle: const Text('Customize keyboard shortcuts'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showKeyboardShortcutsDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildAdvancedSection() {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Advanced',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.bug_report),
|
||||
title: const Text('Debug Logging'),
|
||||
subtitle: const Text('Enable detailed logging for troubleshooting'),
|
||||
value: _enableDebugLogging,
|
||||
onChanged: (value) async {
|
||||
setState(() {
|
||||
_enableDebugLogging = value;
|
||||
});
|
||||
await _settingsService.setEnableDebugLogging(value);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cleaning_services),
|
||||
title: const Text('Clear Cache'),
|
||||
subtitle: const Text('Free up storage space'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showClearCacheDialog(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.restore),
|
||||
title: const Text('Reset Settings'),
|
||||
subtitle: const Text('Reset all settings to defaults'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showResetSettingsDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutSection() {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
title: const Text('About'),
|
||||
subtitle: const Text('App information and licenses'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const AboutScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _showThemeDialog(ThemeProvider themeProvider) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Theme'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<settings.ThemeMode>(
|
||||
title: const Text('System'),
|
||||
subtitle: const Text('Follow system settings'),
|
||||
value: settings.ThemeMode.system,
|
||||
groupValue: themeProvider.themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
themeProvider.setThemeMode(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<settings.ThemeMode>(
|
||||
title: const Text('Light'),
|
||||
value: settings.ThemeMode.light,
|
||||
groupValue: themeProvider.themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
themeProvider.setThemeMode(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile<settings.ThemeMode>(
|
||||
title: const Text('Dark'),
|
||||
value: settings.ThemeMode.dark,
|
||||
groupValue: themeProvider.themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
themeProvider.setThemeMode(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _showBufferSizeDialog(bool isVideo) {
|
||||
final currentSize = isVideo ? _videoBufferSize : _audioBufferSize;
|
||||
final title = isVideo ? 'Video Buffer Size' : 'Audio Buffer Size';
|
||||
final options = isVideo ? [16, 32, 64, 128, 256] : [2, 4, 8, 16, 32];
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: options.map((size) {
|
||||
return RadioListTile<int>(
|
||||
title: Text('${size}MB'),
|
||||
value: size,
|
||||
groupValue: currentSize,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
if (isVideo) {
|
||||
_videoBufferSize = value;
|
||||
_settingsService.setVideoBufferSize(value);
|
||||
} else {
|
||||
_audioBufferSize = value;
|
||||
_settingsService.setAudioBufferSize(value);
|
||||
}
|
||||
});
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showKeyboardShortcutsDialog() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _KeyboardShortcutsScreen(
|
||||
keyboardService: _keyboardService,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearCacheDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Clear Cache'),
|
||||
content: const Text(
|
||||
'This will clear all cached images and data. The app may take longer to load content after clearing the cache.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _settingsService.clearCache();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cache cleared successfully')),
|
||||
);
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showResetSettingsDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Reset Settings'),
|
||||
content: const Text(
|
||||
'This will reset all settings to their default values. This action cannot be undone.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _settingsService.resetAllSettings();
|
||||
await _keyboardService.resetToDefaults();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Settings reset successfully')),
|
||||
);
|
||||
// Reload settings
|
||||
_loadSettings();
|
||||
},
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _KeyboardShortcutsScreen extends StatefulWidget {
|
||||
final KeyboardShortcutsService keyboardService;
|
||||
|
||||
const _KeyboardShortcutsScreen({required this.keyboardService});
|
||||
|
||||
@override
|
||||
State<_KeyboardShortcutsScreen> createState() => _KeyboardShortcutsScreenState();
|
||||
}
|
||||
|
||||
class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> {
|
||||
Map<String, HotKey> _hotkeys = {};
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHotkeys();
|
||||
}
|
||||
|
||||
Future<void> _loadHotkeys() async {
|
||||
await widget.keyboardService.refreshFromStorage();
|
||||
setState(() {
|
||||
_hotkeys = widget.keyboardService.hotkeys;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
title: const Text('Keyboard Shortcuts'),
|
||||
pinned: true,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await widget.keyboardService.resetToDefaults();
|
||||
await _loadHotkeys();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Shortcuts reset to defaults')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final actions = _hotkeys.keys.toList();
|
||||
final action = actions[index];
|
||||
final hotkey = _hotkeys[action]!;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
title: Text(widget.keyboardService.getActionDisplayName(action)),
|
||||
subtitle: Text(action),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
widget.keyboardService.formatHotkey(hotkey),
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
onTap: () => _editHotkey(action, hotkey),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: _hotkeys.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editHotkey(String action, HotKey currentHotkey) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return HotKeyRecorderWidget(
|
||||
actionName: widget.keyboardService.getActionDisplayName(action),
|
||||
currentHotKey: currentHotkey,
|
||||
onHotKeyRecorded: (newHotkey) async {
|
||||
// Check for conflicts
|
||||
final existingAction = widget.keyboardService.getActionForHotkey(newHotkey);
|
||||
if (existingAction != null && existingAction != action) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Shortcut already assigned to ${widget.keyboardService.getActionDisplayName(existingAction)}'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the new hotkey
|
||||
await widget.keyboardService.setHotkey(action, newHotkey);
|
||||
|
||||
if (mounted) {
|
||||
// Update UI directly instead of reloading from storage
|
||||
setState(() {
|
||||
_hotkeys[action] = newHotkey;
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Shortcut updated for ${widget.keyboardService.getActionDisplayName(action)}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel: () => Navigator.pop(context),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'settings_service.dart';
|
||||
|
||||
class KeyboardShortcutsService {
|
||||
static KeyboardShortcutsService? _instance;
|
||||
late SettingsService _settingsService;
|
||||
Map<String, String> _shortcuts = {}; // Legacy string shortcuts for backward compatibility
|
||||
Map<String, HotKey> _hotkeys = {}; // New HotKey objects
|
||||
|
||||
KeyboardShortcutsService._();
|
||||
|
||||
static Future<KeyboardShortcutsService> getInstance() async {
|
||||
if (_instance == null) {
|
||||
_instance = KeyboardShortcutsService._();
|
||||
await _instance!._init();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
_settingsService = await SettingsService.getInstance();
|
||||
// Ensure settings service is fully initialized before loading data
|
||||
await Future.delayed(Duration.zero); // Allow event loop to complete
|
||||
_shortcuts = _settingsService.getKeyboardShortcuts(); // Keep for legacy compatibility
|
||||
_hotkeys = await _settingsService.getKeyboardHotkeys(); // Primary method
|
||||
}
|
||||
|
||||
Map<String, String> get shortcuts => Map.from(_shortcuts);
|
||||
Map<String, HotKey> get hotkeys => Map.from(_hotkeys);
|
||||
|
||||
String getShortcut(String action) {
|
||||
return _shortcuts[action] ?? '';
|
||||
}
|
||||
|
||||
HotKey? getHotkey(String action) {
|
||||
return _hotkeys[action];
|
||||
}
|
||||
|
||||
Future<void> setShortcut(String action, String key) async {
|
||||
_shortcuts[action] = key;
|
||||
await _settingsService.setKeyboardShortcuts(_shortcuts);
|
||||
}
|
||||
|
||||
Future<void> setHotkey(String action, HotKey hotkey) async {
|
||||
// Update local cache first
|
||||
_hotkeys[action] = hotkey;
|
||||
|
||||
// Save to persistent storage
|
||||
await _settingsService.setKeyboardHotkey(action, hotkey);
|
||||
|
||||
// Verify local cache is still correct
|
||||
if (_hotkeys[action] != hotkey) {
|
||||
_hotkeys[action] = hotkey; // Restore correct value
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshFromStorage() async {
|
||||
_hotkeys = await _settingsService.getKeyboardHotkeys();
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
_shortcuts = _settingsService.getDefaultKeyboardShortcuts();
|
||||
_hotkeys = _settingsService.getDefaultKeyboardHotkeys();
|
||||
await _settingsService.setKeyboardShortcuts(_shortcuts);
|
||||
await _settingsService.setKeyboardHotkeys(_hotkeys);
|
||||
// Refresh cache to ensure consistency
|
||||
await refreshFromStorage();
|
||||
}
|
||||
|
||||
// Format HotKey for display
|
||||
String formatHotkey(HotKey? hotKey) {
|
||||
if (hotKey == null) return 'No shortcut set';
|
||||
|
||||
final modifiers = <String>[];
|
||||
for (final modifier in hotKey.modifiers ?? []) {
|
||||
switch (modifier) {
|
||||
case HotKeyModifier.alt:
|
||||
modifiers.add('Alt');
|
||||
break;
|
||||
case HotKeyModifier.control:
|
||||
modifiers.add('Ctrl');
|
||||
break;
|
||||
case HotKeyModifier.shift:
|
||||
modifiers.add('Shift');
|
||||
break;
|
||||
case HotKeyModifier.meta:
|
||||
modifiers.add('Meta');
|
||||
break;
|
||||
case HotKeyModifier.capsLock:
|
||||
modifiers.add('CapsLock');
|
||||
break;
|
||||
case HotKeyModifier.fn:
|
||||
modifiers.add('Fn');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Format the key name
|
||||
String keyName = hotKey.key.keyLabel;
|
||||
if (keyName.startsWith('PhysicalKeyboardKey#')) {
|
||||
keyName = keyName.substring(20, keyName.length - 1);
|
||||
}
|
||||
if (keyName.startsWith('key')) {
|
||||
keyName = keyName.substring(3).toUpperCase();
|
||||
}
|
||||
|
||||
// Special cases for common keys
|
||||
switch (keyName.toLowerCase()) {
|
||||
case 'space':
|
||||
keyName = 'Space';
|
||||
break;
|
||||
case 'arrowup':
|
||||
keyName = 'Arrow Up';
|
||||
break;
|
||||
case 'arrowdown':
|
||||
keyName = 'Arrow Down';
|
||||
break;
|
||||
case 'arrowleft':
|
||||
keyName = 'Arrow Left';
|
||||
break;
|
||||
case 'arrowright':
|
||||
keyName = 'Arrow Right';
|
||||
break;
|
||||
case 'equal':
|
||||
keyName = 'Plus';
|
||||
break;
|
||||
case 'minus':
|
||||
keyName = 'Minus';
|
||||
break;
|
||||
}
|
||||
|
||||
return modifiers.isEmpty ? keyName : '${modifiers.join(' + ')} + $keyName';
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Handle keyboard input for video player
|
||||
KeyEventResult handleVideoPlayerKeyEvent(
|
||||
KeyEvent event,
|
||||
Player player,
|
||||
VoidCallback? onToggleFullscreen,
|
||||
VoidCallback? onToggleSubtitles,
|
||||
VoidCallback? onNextAudioTrack,
|
||||
VoidCallback? onNextSubtitleTrack,
|
||||
VoidCallback? onNextChapter,
|
||||
VoidCallback? onPreviousChapter,
|
||||
) {
|
||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
|
||||
final physicalKey = event.physicalKey;
|
||||
final isShiftPressed = HardwareKeyboard.instance.isShiftPressed;
|
||||
final isControlPressed = HardwareKeyboard.instance.isControlPressed;
|
||||
final isAltPressed = HardwareKeyboard.instance.isAltPressed;
|
||||
final isMetaPressed = HardwareKeyboard.instance.isMetaPressed;
|
||||
|
||||
// Check each hotkey
|
||||
for (final entry in _hotkeys.entries) {
|
||||
final action = entry.key;
|
||||
final hotkey = entry.value;
|
||||
|
||||
// Check if the physical key matches
|
||||
if (physicalKey != hotkey.key) continue;
|
||||
|
||||
// Check if modifiers match
|
||||
final requiredModifiers = hotkey.modifiers ?? [];
|
||||
bool modifiersMatch = true;
|
||||
|
||||
// Check each required modifier
|
||||
for (final modifier in requiredModifiers) {
|
||||
switch (modifier) {
|
||||
case HotKeyModifier.shift:
|
||||
if (!isShiftPressed) modifiersMatch = false;
|
||||
break;
|
||||
case HotKeyModifier.control:
|
||||
if (!isControlPressed) modifiersMatch = false;
|
||||
break;
|
||||
case HotKeyModifier.alt:
|
||||
if (!isAltPressed) modifiersMatch = false;
|
||||
break;
|
||||
case HotKeyModifier.meta:
|
||||
if (!isMetaPressed) modifiersMatch = false;
|
||||
break;
|
||||
case HotKeyModifier.capsLock:
|
||||
// CapsLock is typically not used for shortcuts, ignore for now
|
||||
break;
|
||||
case HotKeyModifier.fn:
|
||||
// Fn key is typically not used for shortcuts, ignore for now
|
||||
break;
|
||||
}
|
||||
if (!modifiersMatch) break;
|
||||
}
|
||||
|
||||
// Check that no extra modifiers are pressed
|
||||
if (modifiersMatch) {
|
||||
final hasShift = requiredModifiers.contains(HotKeyModifier.shift);
|
||||
final hasControl = requiredModifiers.contains(HotKeyModifier.control);
|
||||
final hasAlt = requiredModifiers.contains(HotKeyModifier.alt);
|
||||
final hasMeta = requiredModifiers.contains(HotKeyModifier.meta);
|
||||
|
||||
if (isShiftPressed != hasShift ||
|
||||
isControlPressed != hasControl ||
|
||||
isAltPressed != hasAlt ||
|
||||
isMetaPressed != hasMeta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_executeAction(action, player, onToggleFullscreen, onToggleSubtitles,
|
||||
onNextAudioTrack, onNextSubtitleTrack, onNextChapter, onPreviousChapter);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
void _executeAction(
|
||||
String action,
|
||||
Player player,
|
||||
VoidCallback? onToggleFullscreen,
|
||||
VoidCallback? onToggleSubtitles,
|
||||
VoidCallback? onNextAudioTrack,
|
||||
VoidCallback? onNextSubtitleTrack,
|
||||
VoidCallback? onNextChapter,
|
||||
VoidCallback? onPreviousChapter,
|
||||
) {
|
||||
switch (action) {
|
||||
case 'play_pause':
|
||||
player.playOrPause();
|
||||
break;
|
||||
case 'volume_up':
|
||||
final newVolume = (player.state.volume + 10).clamp(0.0, 100.0);
|
||||
player.setVolume(newVolume);
|
||||
break;
|
||||
case 'volume_down':
|
||||
final newVolume = (player.state.volume - 10).clamp(0.0, 100.0);
|
||||
player.setVolume(newVolume);
|
||||
break;
|
||||
case 'seek_forward':
|
||||
final newPosition = player.state.position + const Duration(seconds: 10);
|
||||
player.seek(newPosition);
|
||||
break;
|
||||
case 'seek_backward':
|
||||
final newPosition = player.state.position - const Duration(seconds: 10);
|
||||
player.seek(newPosition.isNegative ? Duration.zero : newPosition);
|
||||
break;
|
||||
case 'seek_forward_large':
|
||||
final newPosition = player.state.position + const Duration(seconds: 30);
|
||||
player.seek(newPosition);
|
||||
break;
|
||||
case 'seek_backward_large':
|
||||
final newPosition = player.state.position - const Duration(seconds: 30);
|
||||
player.seek(newPosition.isNegative ? Duration.zero : newPosition);
|
||||
break;
|
||||
case 'fullscreen_toggle':
|
||||
onToggleFullscreen?.call();
|
||||
break;
|
||||
case 'mute_toggle':
|
||||
player.setVolume(player.state.volume > 0 ? 0.0 : 100.0);
|
||||
break;
|
||||
case 'subtitle_toggle':
|
||||
onToggleSubtitles?.call();
|
||||
break;
|
||||
case 'audio_track_next':
|
||||
onNextAudioTrack?.call();
|
||||
break;
|
||||
case 'subtitle_track_next':
|
||||
onNextSubtitleTrack?.call();
|
||||
break;
|
||||
case 'chapter_next':
|
||||
onNextChapter?.call();
|
||||
break;
|
||||
case 'chapter_previous':
|
||||
onPreviousChapter?.call();
|
||||
break;
|
||||
case 'speed_increase':
|
||||
final newRate = (player.state.rate + 0.1).clamp(0.1, 3.0);
|
||||
player.setRate(newRate);
|
||||
break;
|
||||
case 'speed_decrease':
|
||||
final newRate = (player.state.rate - 0.1).clamp(0.1, 3.0);
|
||||
player.setRate(newRate);
|
||||
break;
|
||||
case 'speed_reset':
|
||||
player.setRate(1.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get human-readable action names
|
||||
String getActionDisplayName(String action) {
|
||||
switch (action) {
|
||||
case 'play_pause':
|
||||
return 'Play/Pause';
|
||||
case 'volume_up':
|
||||
return 'Volume Up';
|
||||
case 'volume_down':
|
||||
return 'Volume Down';
|
||||
case 'seek_forward':
|
||||
return 'Seek Forward';
|
||||
case 'seek_backward':
|
||||
return 'Seek Backward';
|
||||
case 'seek_forward_large':
|
||||
return 'Seek Forward (Large)';
|
||||
case 'seek_backward_large':
|
||||
return 'Seek Backward (Large)';
|
||||
case 'fullscreen_toggle':
|
||||
return 'Toggle Fullscreen';
|
||||
case 'mute_toggle':
|
||||
return 'Toggle Mute';
|
||||
case 'subtitle_toggle':
|
||||
return 'Toggle Subtitles';
|
||||
case 'audio_track_next':
|
||||
return 'Next Audio Track';
|
||||
case 'subtitle_track_next':
|
||||
return 'Next Subtitle Track';
|
||||
case 'chapter_next':
|
||||
return 'Next Chapter';
|
||||
case 'chapter_previous':
|
||||
return 'Previous Chapter';
|
||||
case 'speed_increase':
|
||||
return 'Increase Speed';
|
||||
case 'speed_decrease':
|
||||
return 'Decrease Speed';
|
||||
case 'speed_reset':
|
||||
return 'Reset Speed';
|
||||
default:
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if a key combination is valid (legacy method for backward compatibility)
|
||||
bool isValidKeyShortcut(String keyString) {
|
||||
// For backward compatibility, assume all non-empty strings are valid
|
||||
// The new system will use HotKey objects for validation
|
||||
return keyString.isNotEmpty;
|
||||
}
|
||||
|
||||
// Check if a shortcut is already assigned to another action
|
||||
String? getActionForShortcut(String keyString) {
|
||||
for (final entry in _shortcuts.entries) {
|
||||
if (entry.value == keyString) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if a hotkey is already assigned to another action
|
||||
String? getActionForHotkey(HotKey hotkey) {
|
||||
for (final entry in _hotkeys.entries) {
|
||||
if (_hotkeyEquals(entry.value, hotkey)) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper method to compare two HotKey objects
|
||||
bool _hotkeyEquals(HotKey a, HotKey b) {
|
||||
if (a.key != b.key) return false;
|
||||
|
||||
final aModifiers = Set.from(a.modifiers ?? []);
|
||||
final bModifiers = Set.from(b.modifiers ?? []);
|
||||
|
||||
return aModifiers.length == bModifiers.length &&
|
||||
aModifiers.every((modifier) => bModifiers.contains(modifier));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
enum ThemeMode { system, light, dark }
|
||||
|
||||
class SettingsService {
|
||||
static const String _keyThemeMode = 'theme_mode';
|
||||
static const String _keyEnableDebugLogging = 'enable_debug_logging';
|
||||
static const String _keyVideoBufferSize = 'video_buffer_size';
|
||||
static const String _keyAudioBufferSize = 'audio_buffer_size';
|
||||
static const String _keyKeyboardShortcuts = 'keyboard_shortcuts';
|
||||
static const String _keyKeyboardHotkeys = 'keyboard_hotkeys';
|
||||
static const String _keyEnableHardwareDecoding = 'enable_hardware_decoding';
|
||||
static const String _keyPreferredVideoCodec = 'preferred_video_codec';
|
||||
static const String _keyPreferredAudioCodec = 'preferred_audio_codec';
|
||||
|
||||
static SettingsService? _instance;
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
SettingsService._();
|
||||
|
||||
static Future<SettingsService> getInstance() async {
|
||||
if (_instance == null) {
|
||||
_instance = SettingsService._();
|
||||
await _instance!._init();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
// Theme Mode
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
await _prefs.setString(_keyThemeMode, mode.name);
|
||||
}
|
||||
|
||||
ThemeMode getThemeMode() {
|
||||
final modeString = _prefs.getString(_keyThemeMode);
|
||||
return ThemeMode.values
|
||||
.firstWhere((mode) => mode.name == modeString, orElse: () => ThemeMode.system);
|
||||
}
|
||||
|
||||
|
||||
// Debug Logging
|
||||
Future<void> setEnableDebugLogging(bool enabled) async {
|
||||
await _prefs.setBool(_keyEnableDebugLogging, enabled);
|
||||
}
|
||||
|
||||
bool getEnableDebugLogging() {
|
||||
return _prefs.getBool(_keyEnableDebugLogging) ?? false;
|
||||
}
|
||||
|
||||
// Video Buffer Size (in MB)
|
||||
Future<void> setVideoBufferSize(int sizeInMB) async {
|
||||
await _prefs.setInt(_keyVideoBufferSize, sizeInMB);
|
||||
}
|
||||
|
||||
int getVideoBufferSize() {
|
||||
return _prefs.getInt(_keyVideoBufferSize) ?? 64; // Default 64MB
|
||||
}
|
||||
|
||||
// Audio Buffer Size (in MB)
|
||||
Future<void> setAudioBufferSize(int sizeInMB) async {
|
||||
await _prefs.setInt(_keyAudioBufferSize, sizeInMB);
|
||||
}
|
||||
|
||||
int getAudioBufferSize() {
|
||||
return _prefs.getInt(_keyAudioBufferSize) ?? 8; // Default 8MB
|
||||
}
|
||||
|
||||
// Hardware Decoding
|
||||
Future<void> setEnableHardwareDecoding(bool enabled) async {
|
||||
await _prefs.setBool(_keyEnableHardwareDecoding, enabled);
|
||||
}
|
||||
|
||||
bool getEnableHardwareDecoding() {
|
||||
return _prefs.getBool(_keyEnableHardwareDecoding) ?? true; // Default enabled
|
||||
}
|
||||
|
||||
// Preferred Video Codec
|
||||
Future<void> setPreferredVideoCodec(String codec) async {
|
||||
await _prefs.setString(_keyPreferredVideoCodec, codec);
|
||||
}
|
||||
|
||||
String getPreferredVideoCodec() {
|
||||
return _prefs.getString(_keyPreferredVideoCodec) ?? 'auto';
|
||||
}
|
||||
|
||||
// Preferred Audio Codec
|
||||
Future<void> setPreferredAudioCodec(String codec) async {
|
||||
await _prefs.setString(_keyPreferredAudioCodec, codec);
|
||||
}
|
||||
|
||||
String getPreferredAudioCodec() {
|
||||
return _prefs.getString(_keyPreferredAudioCodec) ?? 'auto';
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts (Legacy String-based)
|
||||
Map<String, String> getDefaultKeyboardShortcuts() {
|
||||
return {
|
||||
'play_pause': 'Space',
|
||||
'volume_up': 'Arrow Up',
|
||||
'volume_down': 'Arrow Down',
|
||||
'seek_forward': 'Arrow Right',
|
||||
'seek_backward': 'Arrow Left',
|
||||
'seek_forward_large': 'Shift+Arrow Right',
|
||||
'seek_backward_large': 'Shift+Arrow Left',
|
||||
'fullscreen_toggle': 'F',
|
||||
'mute_toggle': 'M',
|
||||
'subtitle_toggle': 'S',
|
||||
'audio_track_next': 'A',
|
||||
'subtitle_track_next': 'Shift+S',
|
||||
'chapter_next': 'N',
|
||||
'chapter_previous': 'P',
|
||||
'speed_increase': 'Plus',
|
||||
'speed_decrease': 'Minus',
|
||||
'speed_reset': 'R',
|
||||
};
|
||||
}
|
||||
|
||||
// HotKey Objects (New implementation)
|
||||
Map<String, HotKey> getDefaultKeyboardHotkeys() {
|
||||
return {
|
||||
'play_pause': HotKey(key: PhysicalKeyboardKey.space),
|
||||
'volume_up': HotKey(key: PhysicalKeyboardKey.arrowUp),
|
||||
'volume_down': HotKey(key: PhysicalKeyboardKey.arrowDown),
|
||||
'seek_forward': HotKey(key: PhysicalKeyboardKey.arrowRight),
|
||||
'seek_backward': HotKey(key: PhysicalKeyboardKey.arrowLeft),
|
||||
'seek_forward_large': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]),
|
||||
'seek_backward_large': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]),
|
||||
'fullscreen_toggle': HotKey(key: PhysicalKeyboardKey.keyF),
|
||||
'mute_toggle': HotKey(key: PhysicalKeyboardKey.keyM),
|
||||
'subtitle_toggle': HotKey(key: PhysicalKeyboardKey.keyS),
|
||||
'audio_track_next': HotKey(key: PhysicalKeyboardKey.keyA),
|
||||
'subtitle_track_next': HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]),
|
||||
'chapter_next': HotKey(key: PhysicalKeyboardKey.keyN),
|
||||
'chapter_previous': HotKey(key: PhysicalKeyboardKey.keyP),
|
||||
'speed_increase': HotKey(key: PhysicalKeyboardKey.equal),
|
||||
'speed_decrease': HotKey(key: PhysicalKeyboardKey.minus),
|
||||
'speed_reset': HotKey(key: PhysicalKeyboardKey.keyR),
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> setKeyboardShortcuts(Map<String, String> shortcuts) async {
|
||||
final jsonString = json.encode(shortcuts);
|
||||
await _prefs.setString(_keyKeyboardShortcuts, jsonString);
|
||||
}
|
||||
|
||||
Map<String, String> getKeyboardShortcuts() {
|
||||
final jsonString = _prefs.getString(_keyKeyboardShortcuts);
|
||||
if (jsonString == null) return getDefaultKeyboardShortcuts();
|
||||
|
||||
try {
|
||||
final decoded = json.decode(jsonString) as Map<String, dynamic>;
|
||||
final shortcuts = decoded.map((key, value) => MapEntry(key, value.toString()));
|
||||
|
||||
// Merge with defaults to ensure all keys exist
|
||||
final defaults = getDefaultKeyboardShortcuts();
|
||||
defaults.addAll(shortcuts);
|
||||
return defaults;
|
||||
} catch (e) {
|
||||
return getDefaultKeyboardShortcuts();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setKeyboardShortcut(String action, String key) async {
|
||||
final shortcuts = getKeyboardShortcuts();
|
||||
shortcuts[action] = key;
|
||||
await setKeyboardShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
String getKeyboardShortcut(String action) {
|
||||
final shortcuts = getKeyboardShortcuts();
|
||||
return shortcuts[action] ?? '';
|
||||
}
|
||||
|
||||
Future<void> resetKeyboardShortcuts() async {
|
||||
await setKeyboardShortcuts(getDefaultKeyboardShortcuts());
|
||||
}
|
||||
|
||||
// HotKey Objects Methods
|
||||
Future<void> setKeyboardHotkeys(Map<String, HotKey> hotkeys) async {
|
||||
final Map<String, Map<String, dynamic>> serializedHotkeys = {};
|
||||
for (final entry in hotkeys.entries) {
|
||||
serializedHotkeys[entry.key] = _serializeHotKey(entry.value);
|
||||
}
|
||||
final jsonString = json.encode(serializedHotkeys);
|
||||
await _prefs.setString(_keyKeyboardHotkeys, jsonString);
|
||||
}
|
||||
|
||||
Future<Map<String, HotKey>> getKeyboardHotkeys() async {
|
||||
final jsonString = _prefs.getString(_keyKeyboardHotkeys);
|
||||
if (jsonString == null) {
|
||||
return getDefaultKeyboardHotkeys();
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = json.decode(jsonString) as Map<String, dynamic>;
|
||||
final Map<String, HotKey> hotkeys = {};
|
||||
|
||||
for (final entry in decoded.entries) {
|
||||
final hotKey = _deserializeHotKey(entry.value as Map<String, dynamic>);
|
||||
if (hotKey != null) {
|
||||
hotkeys[entry.key] = hotKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with defaults to ensure all keys exist, but keep saved hotkeys priority
|
||||
final defaults = getDefaultKeyboardHotkeys();
|
||||
final result = <String, HotKey>{};
|
||||
|
||||
// Start with defaults
|
||||
result.addAll(defaults);
|
||||
// Override with saved hotkeys (this preserves user customizations)
|
||||
result.addAll(hotkeys);
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
return getDefaultKeyboardHotkeys();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setKeyboardHotkey(String action, HotKey hotKey) async {
|
||||
final hotkeys = await getKeyboardHotkeys();
|
||||
hotkeys[action] = hotKey;
|
||||
await setKeyboardHotkeys(hotkeys);
|
||||
}
|
||||
|
||||
Future<HotKey?> getKeyboardHotkey(String action) async {
|
||||
final hotkeys = await getKeyboardHotkeys();
|
||||
return hotkeys[action];
|
||||
}
|
||||
|
||||
Future<void> resetKeyboardHotkeys() async {
|
||||
await setKeyboardHotkeys(getDefaultKeyboardHotkeys());
|
||||
}
|
||||
|
||||
// Helper methods for HotKey serialization
|
||||
Map<String, dynamic> _serializeHotKey(HotKey hotKey) {
|
||||
return {
|
||||
'key': hotKey.key.toString(),
|
||||
'modifiers': hotKey.modifiers?.map((m) => m.name).toList() ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
HotKey? _deserializeHotKey(Map<String, dynamic> data) {
|
||||
try {
|
||||
final keyString = data['key'] as String;
|
||||
final modifierNames = (data['modifiers'] as List<dynamic>).cast<String>();
|
||||
|
||||
final modifiers = modifierNames.map((name) {
|
||||
switch (name) {
|
||||
case 'alt':
|
||||
return HotKeyModifier.alt;
|
||||
case 'control':
|
||||
return HotKeyModifier.control;
|
||||
case 'shift':
|
||||
return HotKeyModifier.shift;
|
||||
case 'meta':
|
||||
return HotKeyModifier.meta;
|
||||
case 'capsLock':
|
||||
return HotKeyModifier.capsLock;
|
||||
case 'fn':
|
||||
return HotKeyModifier.fn;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}).where((m) => m != null).cast<HotKeyModifier>().toList();
|
||||
|
||||
final key = _findKeyByString(keyString);
|
||||
if (key != null) {
|
||||
return HotKey(key: key, modifiers: modifiers.isNotEmpty ? modifiers : null);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore deserialization errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper method to find PhysicalKeyboardKey by string representation
|
||||
PhysicalKeyboardKey? _findKeyByString(String keyString) {
|
||||
|
||||
// Handle exact string matches first for better performance
|
||||
const keyMap = {
|
||||
'PhysicalKeyboardKey#0002c': PhysicalKeyboardKey.space,
|
||||
'PhysicalKeyboardKey#7002a': PhysicalKeyboardKey.backspace,
|
||||
'PhysicalKeyboardKey#7004c': PhysicalKeyboardKey.delete,
|
||||
'PhysicalKeyboardKey#70028': PhysicalKeyboardKey.enter,
|
||||
'PhysicalKeyboardKey#70029': PhysicalKeyboardKey.escape,
|
||||
'PhysicalKeyboardKey#7002b': PhysicalKeyboardKey.tab,
|
||||
'PhysicalKeyboardKey#7004a': PhysicalKeyboardKey.home,
|
||||
'PhysicalKeyboardKey#7004d': PhysicalKeyboardKey.end,
|
||||
'PhysicalKeyboardKey#7004b': PhysicalKeyboardKey.pageUp,
|
||||
'PhysicalKeyboardKey#7004e': PhysicalKeyboardKey.pageDown,
|
||||
'PhysicalKeyboardKey#70050': PhysicalKeyboardKey.arrowLeft,
|
||||
'PhysicalKeyboardKey#70052': PhysicalKeyboardKey.arrowUp,
|
||||
'PhysicalKeyboardKey#7004f': PhysicalKeyboardKey.arrowRight,
|
||||
'PhysicalKeyboardKey#70051': PhysicalKeyboardKey.arrowDown,
|
||||
};
|
||||
|
||||
// Check exact matches first
|
||||
if (keyMap.containsKey(keyString)) {
|
||||
return keyMap[keyString];
|
||||
}
|
||||
|
||||
// Alternative approach: extract USB HID usage code from the toString() output
|
||||
// Format: PhysicalKeyboardKey#ec9ed(usbHidUsage: "0x0007002c", debugName: "Space")
|
||||
try {
|
||||
final usbHidMatch = RegExp(r'usbHidUsage: "0x([0-9a-fA-F]+)"').firstMatch(keyString);
|
||||
if (usbHidMatch != null) {
|
||||
final usbHidCode = usbHidMatch.group(1)!.toLowerCase();
|
||||
|
||||
// Map USB HID codes to PhysicalKeyboardKey objects
|
||||
const usbHidMap = {
|
||||
'0007002c': PhysicalKeyboardKey.space,
|
||||
'0007002a': PhysicalKeyboardKey.backspace,
|
||||
'0007004c': PhysicalKeyboardKey.delete,
|
||||
'00070028': PhysicalKeyboardKey.enter,
|
||||
'00070029': PhysicalKeyboardKey.escape,
|
||||
'0007002b': PhysicalKeyboardKey.tab,
|
||||
'00070039': PhysicalKeyboardKey.capsLock,
|
||||
// Function keys
|
||||
'0007003a': PhysicalKeyboardKey.f1,
|
||||
'0007003b': PhysicalKeyboardKey.f2,
|
||||
'0007003c': PhysicalKeyboardKey.f3,
|
||||
'0007003d': PhysicalKeyboardKey.f4,
|
||||
'0007003e': PhysicalKeyboardKey.f5,
|
||||
'0007003f': PhysicalKeyboardKey.f6,
|
||||
'00070040': PhysicalKeyboardKey.f7,
|
||||
'00070041': PhysicalKeyboardKey.f8,
|
||||
'00070042': PhysicalKeyboardKey.f9,
|
||||
'00070043': PhysicalKeyboardKey.f10,
|
||||
'00070044': PhysicalKeyboardKey.f11,
|
||||
'00070045': PhysicalKeyboardKey.f12,
|
||||
// Number keys
|
||||
'00070027': PhysicalKeyboardKey.digit0,
|
||||
'0007001e': PhysicalKeyboardKey.digit1,
|
||||
'0007001f': PhysicalKeyboardKey.digit2,
|
||||
'00070020': PhysicalKeyboardKey.digit3,
|
||||
'00070021': PhysicalKeyboardKey.digit4,
|
||||
'00070022': PhysicalKeyboardKey.digit5,
|
||||
'00070023': PhysicalKeyboardKey.digit6,
|
||||
'00070024': PhysicalKeyboardKey.digit7,
|
||||
'00070025': PhysicalKeyboardKey.digit8,
|
||||
'00070026': PhysicalKeyboardKey.digit9,
|
||||
// Letter keys
|
||||
'00070004': PhysicalKeyboardKey.keyA,
|
||||
'00070005': PhysicalKeyboardKey.keyB,
|
||||
'00070006': PhysicalKeyboardKey.keyC,
|
||||
'00070007': PhysicalKeyboardKey.keyD,
|
||||
'00070008': PhysicalKeyboardKey.keyE,
|
||||
'00070009': PhysicalKeyboardKey.keyF,
|
||||
'0007000a': PhysicalKeyboardKey.keyG,
|
||||
'0007000b': PhysicalKeyboardKey.keyH,
|
||||
'0007000c': PhysicalKeyboardKey.keyI,
|
||||
'0007000d': PhysicalKeyboardKey.keyJ,
|
||||
'0007000e': PhysicalKeyboardKey.keyK,
|
||||
'0007000f': PhysicalKeyboardKey.keyL,
|
||||
'00070010': PhysicalKeyboardKey.keyM,
|
||||
'00070011': PhysicalKeyboardKey.keyN,
|
||||
'00070012': PhysicalKeyboardKey.keyO,
|
||||
'00070013': PhysicalKeyboardKey.keyP,
|
||||
'00070014': PhysicalKeyboardKey.keyQ,
|
||||
'00070015': PhysicalKeyboardKey.keyR,
|
||||
'00070016': PhysicalKeyboardKey.keyS,
|
||||
'00070017': PhysicalKeyboardKey.keyT,
|
||||
'00070018': PhysicalKeyboardKey.keyU,
|
||||
'00070019': PhysicalKeyboardKey.keyV,
|
||||
'0007001a': PhysicalKeyboardKey.keyW,
|
||||
'0007001b': PhysicalKeyboardKey.keyX,
|
||||
'0007001c': PhysicalKeyboardKey.keyY,
|
||||
'0007001d': PhysicalKeyboardKey.keyZ,
|
||||
// Arrow keys
|
||||
'00070050': PhysicalKeyboardKey.arrowLeft,
|
||||
'00070052': PhysicalKeyboardKey.arrowUp,
|
||||
'0007004f': PhysicalKeyboardKey.arrowRight,
|
||||
'00070051': PhysicalKeyboardKey.arrowDown,
|
||||
// Other common keys
|
||||
'0007002d': PhysicalKeyboardKey.equal,
|
||||
'0007002e': PhysicalKeyboardKey.minus,
|
||||
'0007004a': PhysicalKeyboardKey.home,
|
||||
'0007004d': PhysicalKeyboardKey.end,
|
||||
'0007004b': PhysicalKeyboardKey.pageUp,
|
||||
'0007004e': PhysicalKeyboardKey.pageDown,
|
||||
};
|
||||
|
||||
if (usbHidMap.containsKey(usbHidCode)) {
|
||||
return usbHidMap[usbHidCode];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
|
||||
// Fall back to contains() checks for partial matches
|
||||
if (keyString.contains('space')) {
|
||||
return PhysicalKeyboardKey.space;
|
||||
} else if (keyString.contains('arrowUp')) {
|
||||
return PhysicalKeyboardKey.arrowUp;
|
||||
} else if (keyString.contains('arrowDown')) {
|
||||
return PhysicalKeyboardKey.arrowDown;
|
||||
} else if (keyString.contains('arrowLeft')) {
|
||||
return PhysicalKeyboardKey.arrowLeft;
|
||||
} else if (keyString.contains('arrowRight')) {
|
||||
return PhysicalKeyboardKey.arrowRight;
|
||||
} else if (keyString.contains('equal')) {
|
||||
return PhysicalKeyboardKey.equal;
|
||||
} else if (keyString.contains('minus')) {
|
||||
return PhysicalKeyboardKey.minus;
|
||||
} else if (keyString.contains('escape')) {
|
||||
return PhysicalKeyboardKey.escape;
|
||||
} else if (keyString.contains('enter')) {
|
||||
return PhysicalKeyboardKey.enter;
|
||||
} else if (keyString.contains('tab')) {
|
||||
return PhysicalKeyboardKey.tab;
|
||||
} else if (keyString.contains('backspace')) {
|
||||
return PhysicalKeyboardKey.backspace;
|
||||
} else if (keyString.contains('delete')) {
|
||||
return PhysicalKeyboardKey.delete;
|
||||
} else if (keyString.contains('home')) {
|
||||
return PhysicalKeyboardKey.home;
|
||||
} else if (keyString.contains('end')) {
|
||||
return PhysicalKeyboardKey.end;
|
||||
} else if (keyString.contains('pageUp')) {
|
||||
return PhysicalKeyboardKey.pageUp;
|
||||
} else if (keyString.contains('pageDown')) {
|
||||
return PhysicalKeyboardKey.pageDown;
|
||||
} else {
|
||||
// Try function keys F1-F12
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
if (keyString.contains('f$i') || keyString.contains('F$i')) {
|
||||
switch (i) {
|
||||
case 1: return PhysicalKeyboardKey.f1;
|
||||
case 2: return PhysicalKeyboardKey.f2;
|
||||
case 3: return PhysicalKeyboardKey.f3;
|
||||
case 4: return PhysicalKeyboardKey.f4;
|
||||
case 5: return PhysicalKeyboardKey.f5;
|
||||
case 6: return PhysicalKeyboardKey.f6;
|
||||
case 7: return PhysicalKeyboardKey.f7;
|
||||
case 8: return PhysicalKeyboardKey.f8;
|
||||
case 9: return PhysicalKeyboardKey.f9;
|
||||
case 10: return PhysicalKeyboardKey.f10;
|
||||
case 11: return PhysicalKeyboardKey.f11;
|
||||
case 12: return PhysicalKeyboardKey.f12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try number keys 0-9
|
||||
for (int i = 0; i <= 9; i++) {
|
||||
if (keyString.contains('digit$i') || keyString.contains('Digit$i')) {
|
||||
switch (i) {
|
||||
case 0: return PhysicalKeyboardKey.digit0;
|
||||
case 1: return PhysicalKeyboardKey.digit1;
|
||||
case 2: return PhysicalKeyboardKey.digit2;
|
||||
case 3: return PhysicalKeyboardKey.digit3;
|
||||
case 4: return PhysicalKeyboardKey.digit4;
|
||||
case 5: return PhysicalKeyboardKey.digit5;
|
||||
case 6: return PhysicalKeyboardKey.digit6;
|
||||
case 7: return PhysicalKeyboardKey.digit7;
|
||||
case 8: return PhysicalKeyboardKey.digit8;
|
||||
case 9: return PhysicalKeyboardKey.digit9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try letter keys A-Z (both upper and lower case patterns)
|
||||
const letterKeys = {
|
||||
'A': PhysicalKeyboardKey.keyA, 'B': PhysicalKeyboardKey.keyB, 'C': PhysicalKeyboardKey.keyC,
|
||||
'D': PhysicalKeyboardKey.keyD, 'E': PhysicalKeyboardKey.keyE, 'F': PhysicalKeyboardKey.keyF,
|
||||
'G': PhysicalKeyboardKey.keyG, 'H': PhysicalKeyboardKey.keyH, 'I': PhysicalKeyboardKey.keyI,
|
||||
'J': PhysicalKeyboardKey.keyJ, 'K': PhysicalKeyboardKey.keyK, 'L': PhysicalKeyboardKey.keyL,
|
||||
'M': PhysicalKeyboardKey.keyM, 'N': PhysicalKeyboardKey.keyN, 'O': PhysicalKeyboardKey.keyO,
|
||||
'P': PhysicalKeyboardKey.keyP, 'Q': PhysicalKeyboardKey.keyQ, 'R': PhysicalKeyboardKey.keyR,
|
||||
'S': PhysicalKeyboardKey.keyS, 'T': PhysicalKeyboardKey.keyT, 'U': PhysicalKeyboardKey.keyU,
|
||||
'V': PhysicalKeyboardKey.keyV, 'W': PhysicalKeyboardKey.keyW, 'X': PhysicalKeyboardKey.keyX,
|
||||
'Y': PhysicalKeyboardKey.keyY, 'Z': PhysicalKeyboardKey.keyZ,
|
||||
};
|
||||
|
||||
for (final entry in letterKeys.entries) {
|
||||
if (keyString.contains('key${entry.key}') || keyString.contains('Key${entry.key}')) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Reset all settings to defaults
|
||||
Future<void> resetAllSettings() async {
|
||||
await Future.wait([
|
||||
_prefs.remove(_keyThemeMode),
|
||||
_prefs.remove(_keyEnableDebugLogging),
|
||||
_prefs.remove(_keyVideoBufferSize),
|
||||
_prefs.remove(_keyAudioBufferSize),
|
||||
_prefs.remove(_keyKeyboardShortcuts),
|
||||
_prefs.remove(_keyKeyboardHotkeys),
|
||||
_prefs.remove(_keyEnableHardwareDecoding),
|
||||
_prefs.remove(_keyPreferredVideoCodec),
|
||||
_prefs.remove(_keyPreferredAudioCodec),
|
||||
]);
|
||||
}
|
||||
|
||||
// Clear cache (for storage cleanup)
|
||||
Future<void> clearCache() async {
|
||||
// This would be expanded to clear various cache directories
|
||||
// For now, we'll just clear any cache-related preferences
|
||||
await Future.wait([
|
||||
// Add cache clearing logic here
|
||||
]);
|
||||
}
|
||||
|
||||
// Get all settings as a map for debugging/export
|
||||
Future<Map<String, dynamic>> getAllSettings() async {
|
||||
final hotkeys = await getKeyboardHotkeys();
|
||||
return {
|
||||
'themeMode': getThemeMode().name,
|
||||
'enableDebugLogging': getEnableDebugLogging(),
|
||||
'videoBufferSize': getVideoBufferSize(),
|
||||
'audioBufferSize': getAudioBufferSize(),
|
||||
'enableHardwareDecoding': getEnableHardwareDecoding(),
|
||||
'preferredVideoCodec': getPreferredVideoCodec(),
|
||||
'preferredAudioCodec': getPreferredAudioCodec(),
|
||||
'keyboardShortcuts': getKeyboardShortcuts(),
|
||||
'keyboardHotkeys': hotkeys.map((key, value) => MapEntry(key, _serializeHotKey(value))),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,33 @@ class DesktopWindowPadding {
|
||||
|
||||
/// Right padding for macOS to prevent actions from being too close to edge
|
||||
static const double macOSRight = 16.0;
|
||||
|
||||
/// Right padding for mobile devices to prevent actions from being too close to edge
|
||||
static const double mobileRight = 6.0;
|
||||
}
|
||||
|
||||
/// Helper class for adjusting app bar widgets to account for desktop window controls
|
||||
class DesktopAppBarHelper {
|
||||
/// Builds actions list with appropriate right padding for macOS
|
||||
/// Builds actions list with appropriate right padding for macOS and mobile
|
||||
static List<Widget>? buildAdjustedActions(List<Widget>? actions) {
|
||||
if (!Platform.isMacOS) {
|
||||
double? rightPadding;
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
rightPadding = DesktopWindowPadding.macOSRight;
|
||||
} else if (Platform.isIOS || Platform.isAndroid) {
|
||||
rightPadding = DesktopWindowPadding.mobileRight;
|
||||
}
|
||||
|
||||
// If no platform-specific padding needed, return original actions
|
||||
if (rightPadding == null) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
// macOS: Add padding to keep actions away from edge
|
||||
// Add padding to keep actions away from edge
|
||||
if (actions != null) {
|
||||
return [...actions, SizedBox(width: DesktopWindowPadding.macOSRight)];
|
||||
return [...actions, SizedBox(width: rightPadding)];
|
||||
} else {
|
||||
return [SizedBox(width: DesktopWindowPadding.macOSRight)];
|
||||
return [SizedBox(width: rightPadding)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
class HotKeyRecorderWidget extends StatefulWidget {
|
||||
final String actionName;
|
||||
final HotKey? currentHotKey;
|
||||
final Function(HotKey) onHotKeyRecorded;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const HotKeyRecorderWidget({
|
||||
super.key,
|
||||
required this.actionName,
|
||||
this.currentHotKey,
|
||||
required this.onHotKeyRecorded,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HotKeyRecorderWidget> createState() => _HotKeyRecorderWidgetState();
|
||||
}
|
||||
|
||||
class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
|
||||
HotKey? _recordedHotKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_recordedHotKey = widget.currentHotKey;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Set Shortcut for ${widget.actionName}'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Current shortcut:',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: HotKeyRecorder(
|
||||
initalHotKey: _recordedHotKey,
|
||||
onHotKeyRecorded: (hotKey) {
|
||||
setState(() {
|
||||
_recordedHotKey = hotKey;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_recordedHotKey != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.backspace, size: 18),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_recordedHotKey = null;
|
||||
});
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
tooltip: 'Clear shortcut',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Press any key combination to set a new shortcut',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: widget.onCancel, child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: _recordedHotKey != null
|
||||
? () => widget.onHotKeyRecorded(_recordedHotKey!)
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../models/plex_metadata.dart';
|
||||
import '../models/plex_media_info.dart';
|
||||
import '../providers/plex_client_provider.dart';
|
||||
import '../services/fullscreen_state_manager.dart';
|
||||
import '../services/keyboard_shortcuts_service.dart';
|
||||
import '../utils/desktop_window_padding.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
@@ -56,6 +57,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
Timer? _hideTimer;
|
||||
bool _isFullscreen = false;
|
||||
late final FocusNode _focusNode;
|
||||
KeyboardShortcutsService? _keyboardService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -63,12 +65,46 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
_focusNode = FocusNode();
|
||||
_loadChapters();
|
||||
_startHideTimer();
|
||||
_initKeyboardService();
|
||||
// Add window listener for tracking fullscreen state (for button icon)
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initKeyboardService() async {
|
||||
_keyboardService = await KeyboardShortcutsService.getInstance();
|
||||
}
|
||||
|
||||
void _toggleSubtitles() {
|
||||
// Toggle subtitle visibility - this would need to be implemented based on your subtitle system
|
||||
// For now, this is a placeholder
|
||||
}
|
||||
|
||||
void _nextAudioTrack() {
|
||||
// Switch to next audio track - this would need to be implemented based on your track system
|
||||
// For now, this is a placeholder
|
||||
}
|
||||
|
||||
void _nextSubtitleTrack() {
|
||||
// Switch to next subtitle track - this would need to be implemented based on your subtitle system
|
||||
// For now, this is a placeholder
|
||||
}
|
||||
|
||||
void _nextChapter() {
|
||||
// Go to next chapter - this would use your existing chapter navigation
|
||||
if (widget.onNext != null) {
|
||||
widget.onNext!();
|
||||
}
|
||||
}
|
||||
|
||||
void _previousChapter() {
|
||||
// Go to previous chapter - this would use your existing chapter navigation
|
||||
if (widget.onPrevious != null) {
|
||||
widget.onPrevious!();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideTimer?.cancel();
|
||||
@@ -348,110 +384,18 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
focusNode: _focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) {
|
||||
// Only respond to key down events (not key up)
|
||||
if (event is KeyDownEvent) {
|
||||
final isShiftPressed = HardwareKeyboard.instance.isShiftPressed;
|
||||
final isCtrlPressed = HardwareKeyboard.instance.isControlPressed;
|
||||
if (_keyboardService == null) return KeyEventResult.ignored;
|
||||
|
||||
// Play/Pause shortcuts
|
||||
if (event.logicalKey == LogicalKeyboardKey.space ||
|
||||
event.logicalKey == LogicalKeyboardKey.keyK) {
|
||||
_togglePlayPause();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// Arrow key handling - check for modifiers first
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
if (isCtrlPressed) {
|
||||
// Ctrl+Left: Seek backward 1 minute
|
||||
_seek(const Duration(minutes: -1));
|
||||
} else if (isShiftPressed) {
|
||||
// Shift+Left: Seek backward 5 seconds
|
||||
_seek(const Duration(seconds: -5));
|
||||
} else {
|
||||
// Left: Seek backward 10 seconds
|
||||
_seek(const Duration(seconds: -10));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
if (isCtrlPressed) {
|
||||
// Ctrl+Right: Seek forward 1 minute
|
||||
_seek(const Duration(minutes: 1));
|
||||
} else if (isShiftPressed) {
|
||||
// Shift+Right: Seek forward 5 seconds
|
||||
_seek(const Duration(seconds: 5));
|
||||
} else {
|
||||
// Right: Seek forward 10 seconds
|
||||
_seek(const Duration(seconds: 10));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
// Up: Volume up 5%
|
||||
_adjustVolume(5.0);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
// Down: Volume down 5%
|
||||
_adjustVolume(-5.0);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// J/L keys for seeking (alternative to arrows)
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyJ) {
|
||||
// J: Seek backward 10 seconds
|
||||
_seek(const Duration(seconds: -10));
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyL) {
|
||||
// L: Seek forward 10 seconds
|
||||
_seek(const Duration(seconds: 10));
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// Fullscreen shortcuts
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyF) {
|
||||
_toggleFullscreen();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
// Escape: Exit fullscreen (only if currently fullscreen)
|
||||
if (_isFullscreen) {
|
||||
_toggleFullscreen();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
// Mute shortcut
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyM) {
|
||||
_toggleMute();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// Episode navigation shortcuts
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyN) {
|
||||
// N: Next episode
|
||||
if (widget.onNext != null) {
|
||||
widget.onNext!();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyP) {
|
||||
// P: Previous episode
|
||||
if (widget.onPrevious != null) {
|
||||
widget.onPrevious!();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
return _keyboardService!.handleVideoPlayerKeyEvent(
|
||||
event,
|
||||
widget.player,
|
||||
_toggleFullscreen,
|
||||
_toggleSubtitles,
|
||||
_nextAudioTrack,
|
||||
_nextSubtitleTrack,
|
||||
_nextChapter,
|
||||
_previousChapter,
|
||||
);
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: _showControls
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
@@ -14,6 +15,9 @@
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
||||
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
hotkey_manager_linux
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
screen_retriever_linux
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import hotkey_manager_macos
|
||||
import macos_window_utils
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
@@ -19,6 +20,7 @@ import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- HotKey (0.2.1)
|
||||
- hotkey_manager_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- HotKey
|
||||
- macos_window_utils (1.0.0):
|
||||
- FlutterMacOS
|
||||
- media_kit_libs_macos_video (1.0.4):
|
||||
@@ -30,6 +34,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||
@@ -43,9 +48,15 @@ DEPENDENCIES:
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- HotKey
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
hotkey_manager_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||
macos_window_utils:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos
|
||||
media_kit_libs_macos_video:
|
||||
@@ -73,6 +84,8 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
|
||||
macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d
|
||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||
|
||||
@@ -304,6 +304,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hotkey_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hotkey_manager
|
||||
sha256: "06f0655b76c8dd322fb7101dc615afbdbf39c3d3414df9e059c33892104479cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
hotkey_manager_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_linux
|
||||
sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_macos
|
||||
sha256: "03b5967e64357b9ac05188ea4a5df6fe4ed4205762cb80aaccf8916ee1713c96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_platform_interface
|
||||
sha256: "98ffca25b8cc9081552902747b2942e3bc37855389a4218c9d50ca316b653b13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_windows
|
||||
sha256: "0d03ced9fe563ed0b68f0a0e1b22c9ffe26eb8053cb960e401f68a4f070e0117"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -958,6 +998,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uni_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uni_platform
|
||||
sha256: e02213a7ee5352212412ca026afd41d269eb00d982faa552f419ffc2debfad84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -23,6 +23,7 @@ dependencies:
|
||||
logger: ^2.0.2
|
||||
package_info_plus: ^9.0.0
|
||||
provider: ^6.1.2
|
||||
hotkey_manager: ^0.2.3
|
||||
|
||||
dependency_overrides:
|
||||
media_kit:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
@@ -14,6 +15,8 @@
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
hotkey_manager_windows
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
screen_retriever_windows
|
||||
|
||||
Reference in New Issue
Block a user