Skip to content

commit 3f0775a

abduznik edited this page May 23, 2026 · 1 revision

feat(macos): add macOS platform support

Commit: 3f0775a3f4bb213b48e6b1577171f5eb6e4f3621

Author: Yan

Date: 2026-03-28

Message

  • Add macOS target to emulator registry (PPSSPP, RetroArch)
  • Add macos_url, macos_executable, macos_supported_platforms fields
  • Add macosExecutable getter to EmulatorStrategy base class
  • Add Platform.isMacOS branch to strategy registry filter
  • Add Platform.isMacOS branch to emulator download service
  • Add .dmg extraction via hdiutil attach/detach
  • Add macOS zip extraction via system unzip (preserves symlinks/perms)
  • Auto codesign and remove quarantine after extraction
  • Fix RetroArch launch to use platform-correct paths and separators
  • Fix RetroArch core map to use .dylib on macOS/Linux
  • Fix RetroArch core download URL for macOS (arm64 buildbot)
  • Fix directory service to use getApplicationSupportDirectory on macOS
  • Fix emulator detection to use macos_executable on macOS
  • Fix ROM download state persistence across tab switches
  • Fix setState after dispose in emulator download listener
  • Disable app sandbox in DebugProfile for dev builds

Why: Adds a new feature or capability to the application.

Files Changed

.gitignore                                         |   1 +
 ios/Flutter/Debug.xcconfig                         |   1 +
 ios/Flutter/Release.xcconfig                       |   1 +
 ios/Podfile                                        |  43 +++++++++
 lib/core/emulator/emulator_download_service.dart   |  10 +-
 lib/core/emulator/emulator_registry_data.dart      |  13 ++-
 lib/core/emulator/emulator_strategy.dart           |   2 +
 lib/core/emulator/strategies/ppsspp_strategy.dart  |   4 +
 .../emulator/strategies/retroarch_strategy.dart    |  87 ++++++++++-------
 lib/core/emulator/strategy_registry.dart           |   1 +
 lib/core/extraction/extraction_service.dart        | 100 ++++++++++++++++++--
 lib/core/storage/directory_service.dart            |  63 +++++++++----
 lib/ui/screens/library_screen.dart                 |  24 +++++
 lib/ui/screens/settings_emulators_section.dart     |  25 +++--
 lib/ui/screens/settings_screen.dart                |  10 +-
 macos/Flutter/Flutter-Debug.xcconfig               |   1 +
 macos/Flutter/Flutter-Release.xcconfig             |   1 +
 macos/Podfile                                      |  42 +++++++++
 macos/Podfile.lock                                 |  42 +++++++++
 macos/Runner.xcodeproj/project.pbxproj             | 103 ++++++++++++++++++++-
 macos/Runner.xcworkspace/contents.xcworkspacedata  |   3 +
 macos/Runner/DebugProfile.entitlements             |  18 ++--
 macos/Runner/Release.entitlements                  |  16 +++-
 23 files changed, 528 insertions(+), 83 deletions(-)
  • .gitignore
  • ios/Flutter/Debug.xcconfig
  • ios/Flutter/Release.xcconfig
  • ios/Podfile
  • lib/core/emulator/emulator_download_service.dart
  • lib/core/emulator/emulator_registry_data.dart
  • lib/core/emulator/emulator_strategy.dart
  • lib/core/emulator/strategies/ppsspp_strategy.dart
  • lib/core/emulator/strategies/retroarch_strategy.dart
  • lib/core/emulator/strategy_registry.dart
  • lib/core/extraction/extraction_service.dart
  • lib/core/storage/directory_service.dart
  • lib/ui/screens/library_screen.dart
  • lib/ui/screens/settings_emulators_section.dart
  • lib/ui/screens/settings_screen.dart
  • macos/Flutter/Flutter-Debug.xcconfig
  • macos/Flutter/Flutter-Release.xcconfig
  • macos/Podfile
  • macos/Podfile.lock
  • macos/Runner.xcodeproj/project.pbxproj
  • macos/Runner.xcworkspace/contents.xcworkspacedata
  • macos/Runner/DebugProfile.entitlements
  • macos/Runner/Release.entitlements

Diff

diff --git a/.gitignore b/.gitignore
index ea0b2ab..69a85da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,4 @@ app.*.map.json
 /android/app/profile
 /android/app/release
 .aider*
+Documents/
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
index 592ceee..ec97fc6 100644
--- a/ios/Flutter/Debug.xcconfig
+++ b/ios/Flutter/Debug.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
 #include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
index 592ceee..c4855bf 100644
--- a/ios/Flutter/Release.xcconfig
+++ b/ios/Flutter/Release.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
 #include "Generated.xcconfig"
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..620e46e
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+
+  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+  target 'RunnerTests' do
+    inherit! :search_paths
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_ios_build_settings(target)
+  end
+end
diff --git a/lib/core/emulator/emulator_download_service.dart b/lib/core/emulator/emulator_download_service.dart
index 968a204..990da4b 100644
--- a/lib/core/emulator/emulator_download_service.dart
+++ b/lib/core/emulator/emulator_download_service.dart
@@ -62,9 +62,13 @@ class EmulatorDownloadService {
         return;
       }
     } else {
-      downloadUrl = Platform.isWindows
-          ? definition['windows_url'] as String?
-          : definition['linux_url'] as String?;
+      if (Platform.isWindows) {
+        downloadUrl = definition['windows_url'] as String?;
+      } else if (Platform.isMacOS) {
+        downloadUrl = definition['macos_url'] as String?;
+      } else {
+        downloadUrl = definition['linux_url'] as String?;
+      }
     }
 
     if (downloadUrl == null) {
diff --git a/lib/core/emulator/emulator_registry_data.dart b/lib/core/emulator/emulator_registry_data.dart
index 54ba544..3766fbe 100644
--- a/lib/core/emulator/emulator_registry_data.dart
+++ b/lib/core/emulator/emulator_registry_data.dart
@@ -3,16 +3,19 @@ const List<Map<String, dynamic>> kEmulatorDefinitions = [
     'id': 'retroarch',
     'name': 'RetroArch',
     'type': 'direct',
-    'windows_url': 'https://buildbot.libretro.com/stable/1.19.1/windows/x86_64/RetroArch.7z',
+    'windows_url': 'https://buildbot.libretro.com/stable/2.0/windows/x86_64/RetroArch_x64.zip',
     'windows_executable': 'RetroArch.exe',
+    'linux_url': 'https://buildbot.libretro.com/stable/2.0/linux/x86_64/RetroArch.zip',
     'linux_executable': 'retroarch',
+    'macos_url': 'https://buildbot.libretro.com/stable/1.22.2/apple/osx/universal/RetroArch_Metal.dmg',
+    'macos_executable': 'RetroArch.app/Contents/MacOS/RetroArch',
     'platform_slugs': [
       'gba', 'gbc', 'gb', 'nes', 'snes', 'n64', 'nds', 'psx', 'ps1', 'playstation',
       'psp', 'dc', 'dreamcast', 'segacd', 'saturn', 'megadrive', 'genesis', 'md',
       'gamegear', 'atari2600', 'atari7800', 'lynx', 'neogeo', 'arcade', 'mame',
       'pcengine', 'wonderswan', 'virtualboy', 'msx', 'dos'
     ],
-    'supported_platforms': ['windows', 'linux'],
+    'supported_platforms': ['windows', 'linux', 'macos'],
   },
   {
     'id': 'dolphin',
@@ -145,13 +148,15 @@ const List<Map<String, dynamic>> kEmulatorDefinitions = [
   {
     'id': 'ppsspp',
     'name': 'PPSSPP (PSP)',
-    'type': 'github',
+    'type': 'direct',
     'github_repo': 'hrydgard/ppsspp',
     'github_asset_required': ['Windows', 'x64', '.zip'],
     'github_asset_excluded': ['debug', 'symbols', 'VR'],
     'windows_executable': 'PPSSPPWindows64.exe',
     'linux_executable': 'PPSSPP',
-    'supported_platforms': ['windows', 'linux'],
+    'macos_url': 'https://github.com/hrydgard/ppsspp/releases/download/v1.20.3/PPSSPPSDL-macOS-v1.20.3.zip',
+    'macos_executable': 'PPSSPPSDL.app/Contents/MacOS/PPSSPPSDL',
+    'supported_platforms': ['windows', 'linux', 'macos'],
     'platform_slugs': ['psp', 'playstation-portable'],
   },
   {
diff --git a/lib/core/emulator/emulator_strategy.dart b/lib/core/emulator/emulator_strategy.dart
index b0e70f9..5648e3c 100644
--- a/lib/core/emulator/emulator_strategy.dart
+++ b/lib/core/emulator/emulator_strategy.dart
@@ -8,11 +8,13 @@ abstract class EmulatorStrategy {
   List<String> get supportedSlugs;
   String get windowsExecutable;
   String get linuxExecutable;
+  String get macosExecutable => windowsExecutable;
   bool get supportsSaveSync;
 
   String getExecutableForPlatform() {
     if (io.Platform.isWindows) return windowsExecutable;
     if (io.Platform.isLinux) return linuxExecutable;
+    if (io.Platform.isMacOS) return macosExecutable;
     return windowsExecutable;
   }
 
diff --git a/lib/core/emulator/strategies/ppsspp_strategy.dart b/lib/core/emulator/strategies/ppsspp_strategy.dart
index 394f752..183d10f 100644
--- a/lib/core/emulator/strategies/ppsspp_strategy.dart
+++ b/lib/core/emulator/strategies/ppsspp_strategy.dart
@@ -1,4 +1,5 @@
 import 'dart:io';
+import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -23,6 +24,9 @@ class PPSSPPStrategy extends EmulatorStrategy {
   @override
   String get linuxExecutable => 'PPSSPP';
 
+  @override
+  String get macosExecutable => 'PPSSPPSDL.app/Contents/MacOS/PPSSPPSDL';
+
   @override
   bool get supportsSaveSync => false;
 
diff --git a/lib/core/emulator/strategies/retroarch_strategy.dart b/lib/core/emulator/strategies/retroarch_strategy.dart
index dbff3b6..204db24 100644
--- a/lib/core/emulator/strategies/retroarch_strategy.dart
+++ b/lib/core/emulator/strategies/retroarch_strategy.dart
@@ -1,4 +1,5 @@
-import 'dart:io';
+import 'dart:io' as io show Platform, File, Directory;
+import 'dart:io' show Process, ProcessStartMode;
 import 'package:dio/dio.dart';
 import 'package:path/path.dart' as p;
 import 'package:path_provider/path_provider.dart';
@@ -51,10 +52,13 @@ class RetroArchStrategy extends EmulatorStrategy {
   @override
   String get linuxExecutable => 'retroarch';
 
+  @override
+  String get macosExecutable => 'RetroArch.app/Contents/MacOS/RetroArch';
+
   @override
   bool get supportsSaveSync => true;
 
-  static const Map<String, String> _coreMap = {
+  static const Map<String, String> _coreMapWindows = {
     'gba': 'mgba_libretro.dll',
     'gbc': 'mgba_libretro.dll',
     'gb': 'mgba_libretro.dll',
@@ -66,9 +70,22 @@ class RetroArchStrategy extends EmulatorStrategy {
     'dc': 'flycast_libretro.dll',
   };
 
+  static const Map<String, String> _coreMapUnix = {
+    'gba': 'mgba_libretro.dylib',
+    'gbc': 'mgba_libretro.dylib',
+    'gb': 'mgba_libretro.dylib',
+    'snes': 'snes9x_libretro.dylib',
+    'nds': 'desmume2015_libretro.dylib',
+    'n64': 'mupen64plus_next_libretro.dylib',
+    'psx': 'pcsx_rearmed_libretro.dylib',
+    'psp': 'ppsspp_libretro.dylib',
+    'dc': 'flycast_libretro.dylib',
+  };
+
   String? _getCoreForSlug(String? slug) {
     if (slug == null) return null;
-    return _coreMap[slug.toLowerCase()];
+    final map = io.Platform.isWindows ? _coreMapWindows : _coreMapUnix;
+    return map[slug.toLowerCase()];
   }
 
   @override
@@ -79,33 +96,28 @@ class RetroArchStrategy extends EmulatorStrategy {
       throw Exception('$name not found. Please download it first.');
     }
 
-    final normalizedExe = exePath.replaceAll('/', r'\');
-    final normalizedRom = romPath.replaceAll('/', r'\');
     final coreName = _getCoreForSlug(game.platformSlug);
+    final sep = io.Platform.isWindows ? r'\' : '/';
 
     if (coreName == null) {
-      await Process.start(
-        normalizedExe,
-        [normalizedRom],
-        mode: ProcessStartMode.detached,
-      );
+      await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
       return;
     }
 
-    final exeDir = File(normalizedExe).parent.path;
-    final corePath = '$exeDir\\cores\\$coreName';
+    final exeDir = io.File(exePath).parent.path;
+    final corePath = '$exeDir${sep}cores$sep$coreName';
 
-    if (!await File(corePath).exists()) {
+    if (!await io.File(corePath).exists()) {
       throw MissingRetroArchCoreException(
         coreName: coreName,
         corePath: corePath,
-        exePath: normalizedExe,
+        exePath: exePath,
       );
     }
 
     await Process.start(
-      normalizedExe,
-      ['-L', corePath, normalizedRom],
+      exePath,
+      ['-L', corePath, romPath],
       mode: ProcessStartMode.detached,
     );
   }
@@ -118,54 +130,61 @@ class RetroArchStrategy extends EmulatorStrategy {
       throw Exception('$name not found. Please download it first.');
     }
 
-    final normalizedExe = exePath.replaceAll('/', r'\');
-    final normalizedRom = romPath.replaceAll('/', r'\');
     final coreName = _getCoreForSlug(game.platformSlug);
+    final sep = io.Platform.isWindows ? r'\' : '/';
 
     if (coreName == null) {
-      return await Process.start(
-        normalizedExe,
-        [normalizedRom],
-        mode: ProcessStartMode.normal,
-      );
+      return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
     }
 
-    final exeDir = File(normalizedExe).parent.path;
-    final corePath = '$exeDir\\cores\\$coreName';
+    final exeDir = io.File(exePath).parent.path;
+    final corePath = '$exeDir${sep}cores$sep$coreName';
 
-    if (!await File(corePath).exists()) {
+    if (!await io.File(corePath).exists()) {
       throw MissingRetroArchCoreException(
         coreName: coreName,
         corePath: corePath,
-        exePath: normalizedExe,
+        exePath: exePath,
       );
     }
 
     return await Process.start(
-      normalizedExe,
-      ['-L', corePath, normalizedRom],
+      exePath,
+      ['-L', corePath, romPath],
       mode: ProcessStartMode.normal,
     );
   }
 
   Future<void> downloadCore(String coreName, String coresDir, Dio dio) async {
-    final url = 'https://buildbot.libretro.com/nightly/windows/x86_64/latest/$coreName.zip';
+    final String url;
+    final String ext;
+    if (io.Platform.isWindows) {
+      ext = 'dll';
+      url = 'https://buildbot.libretro.com/nightly/windows/x86_64/latest/$coreName.zip';
+    } else if (io.Platform.isMacOS) {
+      ext = 'dylib';
+      url = 'https://buildbot.libretro.com/nightly/apple/osx/arm64/latest/$coreName.zip';
+    } else {
+      ext = 'so';
+      url = 'https://buildbot.libretro.com/nightly/linux/x86_64/latest/$coreName.zip';
+    }
+
     final tempDir = await getTemporaryDirectory();
     final zipPath = p.join(tempDir.path, '$coreName.zip');
 
     try {
       await dio.download(url, zipPath);
-      final bytes = await File(zipPath).readAsBytes();
+      final bytes = await io.File(zipPath).readAsBytes();
       final archive = ZipDecoder().decodeBytes(bytes);
       for (final entry in archive) {
-        if (entry.isFile && entry.name.endsWith('.dll')) {
-          final outFile = File('$coresDir\\${entry.name}');
+        if (entry.isFile && entry.name.endsWith('.$ext')) {
+          final outFile = io.File(p.join(coresDir, entry.name));
           await outFile.parent.create(recursive: true);
           await outFile.writeAsBytes(entry.content as List<int>);
         }
       }
     } finally {
-      final f = File(zipPath);
+      final f = io.File(zipPath);
       if (await f.exists()) await f.delete();
     }
   }
diff --git a/lib/core/emulator/strategy_registry.dart b/lib/core/emulator/strategy_registry.dart
index b60d6cf..a9859a8 100644
--- a/lib/core/emulator/strategy_registry.dart
+++ b/lib/core/emulator/strategy_registry.dart
@@ -52,6 +52,7 @@ class StrategyRegistry {
       final supported = List<String>.from(definition['supported_platforms'] ?? []);
       if (Platform.isWindows && supported.contains('windows')) return true;
       if (Platform.isLinux && supported.contains('linux')) return true;
+      if (Platform.isMacOS && supported.contains('macos')) return true;
       return false;
     }).toList();
   }
diff --git a/lib/core/extraction/extraction_service.dart b/lib/core/extraction/extraction_service.dart
index 5880ed9..8c85d1a 100644
--- a/lib/core/extraction/extraction_service.dart
+++ b/lib/core/extraction/extraction_service.dart
@@ -18,9 +18,73 @@ class ExtractionService {
   Future<void> extract(String archivePath, String destDir) async {
     final pathLower = archivePath.toLowerCase();
 
+    if (pathLower.endsWith('.dmg')) {
+      final mountResult = await Process.run(
+        'hdiutil',
+        ['attach', archivePath, '-nobrowse', '-readonly'],
+      );
+      if (mountResult.exitCode != 0) {
+        throw Exception('Failed to mount DMG: ${mountResult.stderr}');
+      }
+
+      String? mountPoint;
+      for (final line in mountResult.stdout.toString().split('\n')) {
+        if (line.contains('/Volumes/')) {
+          mountPoint = line.trim().split('\t').last.trim();
+          break;
+        }
+      }
+      if (mountPoint == null) {
+        throw Exception('Could not determine DMG mount point');
+      }
+
+      try {
+        final volume = Directory(mountPoint);
+        await for (final entity in volume.list()) {
+          if (entity is Directory && entity.path.endsWith('.app')) {
+            final appName = entity.uri.pathSegments
+                .where((s) => s.isNotEmpty)
+                .last;
+            final dest = Directory('$destDir/$appName');
+            await _copyDirectory(entity, dest);
+            break;
+          }
+        }
+      } finally {
+        await Process.run('hdiutil', ['detach', mountPoint, '-force']);
+      }
+      return;
+    }
+
     if (pathLower.endsWith('.zip')) {
-      final fileBytes = await File(archivePath).readAsBytes();
-      await compute(_extractZipIsolate, [fileBytes, destDir]);
+      if (Platform.isMacOS || Platform.isLinux) {
+        final result = await Process.run(
+          'unzip',
+          ['-o', archivePath, '-d', destDir],
+          runInShell: false,
+        );
+        if (result.exitCode != 0) {
+          throw Exception('unzip failed: ${result.stderr}');
+        }
+        await Process.run(
+          'find',
+          [destDir, '-name', '*.app', '-exec', 'chmod', '-R', '+x', '{}', ';'],
+          runInShell: false,
+        );
+        // Remove quarantine and self-sign all .app bundles so macOS allows launch
+        final findResult = await Process.run(
+          'find', [destDir, '-name', '*.app', '-maxdepth', '3'],
+          runInShell: false,
+        );
+        for (final appPath in findResult.stdout.toString().trim().split('\n')) {
+          if (appPath.isEmpty) continue;
+          await Process.run('xattr', ['-rd', 'com.apple.quarantine', appPath]);
+          await Process.run('codesign', ['--force', '--deep', '--sign', '-', appPath]);
+        }
+      } else {
+        final fileBytes = await File(archivePath).readAsBytes();
+        await compute(_extractZipIsolate, [fileBytes, destDir]);
+      }
     } else if (pathLower.endsWith('.7z')) {
       final sevenZipExe = await directoryService.resolveSevenZipPath();
       if (sevenZipExe == null) {
@@ -35,14 +99,12 @@ class ExtractionService {
         throw Exception('7z extraction failed: ${result.stderr}');
       }
     } else if (pathLower.endsWith('.exe')) {
-      // Self-extracting archive
       var result = await Process.run(
         archivePath,
         ['-o$destDir', '-y'],
         runInShell: false,
       );
       if (result.exitCode != 0) {
-        // Try as a plain self-extractor with no arguments
         result = await Process.run(
           archivePath,
           [],
@@ -50,7 +112,6 @@ class ExtractionService {
         );
       }
     } else {
-      // Try ZIP magic bytes (PK = 0x50 0x4B)
       bool isZip = false;
       try {
         final raf = await File(archivePath).open();
@@ -60,11 +121,36 @@ class ExtractionService {
       } catch (_) {}
 
       if (isZip) {
-        final fileBytes = await File(archivePath).readAsBytes();
-        await compute(_extractZipIsolate, [fileBytes, destDir]);
+        if (Platform.isMacOS || Platform.isLinux) {
+          final result = await Process.run(
+            'unzip',
+            ['-o', archivePath, '-d', destDir],
+            runInShell: false,
+          );
+          if (result.exitCode != 0) {
+            throw Exception('unzip failed: ${result.stderr}');
+          }
+        } else {
+          final fileBytes = await File(archivePath).readAsBytes();
+          await compute(_extractZipIsolate, [fileBytes, destDir]);
+        }
       } else {
         throw Exception('Unsupported archive format: $archivePath');
       }
     }
   }
+
+  Future<void> _copyDirectory(Directory source, Directory dest) async {
+    await dest.create(recursive: true);
+    await for (final entity in source.list(recursive: false)) {
+      final name = entity.uri.pathSegments
+          .where((s) => s.isNotEmpty)
+          .last;
+      if (entity is Directory) {
+        await _copyDirectory(entity, Directory('${dest.path}/$name'));
+      } else if (entity is File) {
+        await entity.copy('${dest.path}/$name');
+      }
+    }
+  }
 }
diff --git a/lib/core/storage/directory_service.dart b/lib/core/storage/directory_service.dart
index e71e873..a6badb0 100644
--- a/lib/core/storage/directory_service.dart
+++ b/lib/core/storage/directory_service.dart
@@ -1,6 +1,7 @@
 import 'dart:io';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
+import 'package:path_provider/path_provider.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 
@@ -42,8 +43,18 @@ class DirectoryService {
 
   Future<void> initialize() async {
     final prefs = await SharedPreferences.getInstance();
-    romsRootPath = prefs.getString(_romsRootPathKey) ?? _defaultRomsPath;
-    emulatorsRootPath = prefs.getString(_emulatorsRootPathKey) ?? _defaultEmulatorsPath;
+    final String defaultBase;
+    if (defaultTargetPlatform == TargetPlatform.macOS ||
+        defaultTargetPlatform == TargetPlatform.linux) {
+      final appSupport = await getApplicationSupportDirectory();
+      defaultBase = appSupport.path;
+    } else {
+      final docsDir = await getApplicationDocumentsDirectory();
+      defaultBase = docsDir.path;
+    }
+
+    romsRootPath = prefs.getString(_romsRootPathKey) ?? '$defaultBase/ROMs';
+    emulatorsRootPath = prefs.getString(_emulatorsRootPathKey) ?? '$defaultBase/Emulators';
     await _ensureDirectoryExists(romsRootPath);
     await _ensureDirectoryExists(emulatorsRootPath);
     await loadEmulatorPathOverrides();
@@ -219,32 +230,50 @@ class DirectoryService {
     return '$emulatorDir/$executableName';
   }
 
-  Future<bool> isEmulatorInstalled(String emulatorId, String executableName) async {
-    final dir = Directory(await getEmulatorDirectory(emulatorId));
-    if (!await dir.exists()) return false;
-    if (await File('${dir.path}/$executableName').exists()) return true;
-    await for (final entity in dir.list()) {
-      if (entity is Directory) {
-        if (await File('${entity.path}/$executableName').exists()) return true;
+  Future<String?> findEmulatorExecutable(String emulatorId, String executableName) async {
+    final emulatorDir = await getEmulatorDirectory(emulatorId);
+    final dir = Directory(emulatorDir);
+    if (!await dir.exists()) {
+      return null;
+    }
+
+    final direct = File('$emulatorDir/$executableName');
+    if (await direct.exists()) {
+      return direct.path;
+    }
+
+    if (executableName.contains('/')) {
+      await for (final entity in dir.list()) {
+        if (entity is Directory) {
+          final sub = File('${entity.path}/$executableName');
+          if (await sub.exists()) {
+            return sub.path;
+          }
+        }
       }
+      return null;
     }
-    return false;
-  }
 
-  Future<String?> findEmulatorExecutable(String emulatorId, String executableName) async {
-    final dir = Directory(await getEmulatorDirectory(emulatorId));
-    if (!await dir.exists()) return null;
-    final direct = File('${dir.path}/$executableName');
-    if (await direct.exists()) return direct.path;
     await for (final entity in dir.list()) {
+      if (entity is File && entity.path.endsWith('/$executableName')) {
+        return entity.path;
+      }
       if (entity is Directory) {
         final sub = File('${entity.path}/$executableName');
-        if (await sub.exists()) return sub.path;
+        if (await sub.exists()) {
+          return sub.path;
+        }
       }
     }
+
     return null;
   }
 
+  Future<bool> isEmulatorInstalled(String emulatorId, String executableName) async {
+    final found = await findEmulatorExecutable(emulatorId, executableName);
+    return found != null;
+  }
+
   Future<bool> isRomDownloaded(Game game) async {
     final found = await findExistingRomPath(game);
     return found != null;
diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart
index 45fd81b..dadd284 100644
--- a/lib/ui/screens/library_screen.dart
+++ b/lib/ui/screens/library_screen.dart
@@ -41,6 +41,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> {
     _scrollController.addListener(_onScroll);
     WidgetsBinding.instance.addPostFrameCallback((_) {
       _focusNode.requestFocus();
+      _refreshAllDownloadStates();
     });
   }
 
@@ -76,6 +77,24 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> {
       platformId: ref.read(selectedPlatformIdProvider)?.toString(),
       search: ref.read(searchQueryProvider).isEmpty ? null : ref.read(searchQueryProvider),
     );
+    await _refreshAllDownloadStates();
+  }
+
+  Future<void> _refreshAllDownloadStates() async {
+    if (ref.read(paginatedGamesProvider).games.isEmpty) return;
+    final dirService = ref.read(directoryServiceProvider).asData?.value;
+    if (dirService == null) return;
+    final games = ref.read(paginatedGamesProvider).games;
+    final results = await Future.wait(
+      games.map((g) async => MapEntry(g.id, await dirService.isRomDownloaded(g))),
+    );
+    if (mounted) {
+      setState(() {
+        for (final entry in results) {
+          _downloadedStates[entry.key] = entry.value;
+        }
+      });
+    }
   }
 
   void _startDownload(BuildContext context, WidgetRef ref, Game game) {
@@ -525,6 +544,11 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> {
                                         ));
                                       }
                                       final game = paginatedState.games[index];
+                                      if (_downloadedStates[game.id] == null) {
+                                        WidgetsBinding.instance.addPostFrameCallback((_) {
+                                          _refreshAllDownloadStates();
+                                        });
+                                      }
                                       final dirService = directoryServiceAsync.asData?.value;
                                       final isWindowsGame = ['windows', 'pc', 'win'].contains(game.platformSlug?.toLowerCase() ?? '');
                                       final coverUrl = ref.read(rommServiceProvider)?.resolveCoverUrl(game);
diff --git a/lib/ui/screens/settings_emulators_section.dart b/lib/ui/screens/settings_emulators_section.dart
index 4fc673a..b87ea52 100644
--- a/lib/ui/screens/settings_emulators_section.dart
+++ b/lib/ui/screens/settings_emulators_section.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:file_picker/file_picker.dart';
@@ -78,15 +79,23 @@ Widget buildEmulatorsSection(
                           ref.read(downloadProvider.notifier).startEmulatorDownload(emulatorId, emulatorName);
 
                           // Listen for download completion and update state
-                          ref.read(downloadProvider.notifier).stream.listen((downloads) {
+                          StreamSubscription? sub;
+                          sub = ref.read(downloadProvider.notifier).stream.listen((downloads) {
                             final progress = downloads[emulatorId];
-                            if (progress != null && progress.isComplete) { // Use safeContext here
-                              // Update the map directly. setState will re-render using the updated map.
-                              emulatorInstallStates[emulatorId] = true;
-                              setState(() {}); // Trigger parent state update
-                              messenger.showSnackBar(SnackBar(
-                                content: Text('$emulatorName downloaded.'),
-                              ));
+                            if (progress != null && (progress.isComplete || progress.error != null)) {
+                              if (progress.isComplete) {
+                                emulatorInstallStates[emulatorId] = true;
+                                if (context.mounted) setState(() {});
+                                messenger.showSnackBar(SnackBar(
+                                  content: Text('$emulatorName downloaded successfully.'),
+                                ));
+                              } else if (progress.error != null) {
+                                if (context.mounted) setState(() {});
+                                messenger.showSnackBar(SnackBar(
+                                  content: Text('Failed to download $emulatorName: ${progress.error}'),
+                                ));
+                              }
+                              sub?.cancel();
                             }
                           });
                         },
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index f75081b..f41ca2d 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:file_picker/file_picker.dart';
@@ -62,7 +63,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
     final states = <String, bool>{};
     for (final def in kEmulatorDefinitions) {
       final id = def['id'] as String;
-      final exe = def['windows_executable'] as String;
+      final String exe;
+      if (defaultTargetPlatform == TargetPlatform.macOS) {
+        exe = (def['macos_executable'] as String?) ?? (def['windows_executable'] as String? ?? '');
+      } else if (defaultTargetPlatform == TargetPlatform.linux) {
+        exe = (def['linux_executable'] as String?) ?? '';
+      } else {
+        exe = (def['windows_executable'] as String?) ?? '';
+      }
       if (exe.isEmpty) {
         states[id] = true; // Assume installed if no executable is defined
         continue;
diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig
index c2efd0b..4b81f9b 100644
--- a/macos/Flutter/Flutter-Debug.xcconfig
+++ b/macos/Flutter/Flutter-Debug.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
 #include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig
index c2efd0b..5caa9d1 100644
--- a/macos/Flutter/Flutter-Release.xcconfig
+++ b/macos/Flutter/Flutter-Release.xcconfig
@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
 #include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/macos/Podfile b/macos/Podfile
new file mode 100644
index 0000000..ff5ddb3
--- /dev/null
+++ b/macos/Podfile
@@ -0,0 +1,42 @@
+platform :osx, '10.15'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+
+  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+  target 'RunnerTests' do
+    inherit! :search_paths
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_macos_build_settings(target)
+  end
+end
diff --git a/macos/Podfile.lock b/macos/Podfile.lock
new file mode 100644
index 0000000..76836e7
--- /dev/null
+++ b/macos/Podfile.lock
@@ -0,0 +1,42 @@
+PODS:
+  - flutter_secure_storage_macos (6.1.3):
+    - FlutterMacOS
+  - FlutterMacOS (1.0.0)
+  - package_info_plus (0.0.1):
+    - FlutterMacOS
+  - shared_preferences_foundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
+  - sqflite_darwin (0.0.4):
+    - Flutter
+    - FlutterMacOS
+
+DEPENDENCIES:
+  - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
+  - FlutterMacOS (from `Flutter/ephemeral`)
+  - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
+  - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
+  - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
+
+EXTERNAL SOURCES:
+  flutter_secure_storage_macos:
+    :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
+  FlutterMacOS:
+    :path: Flutter/ephemeral
+  package_info_plus:
+    :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
+  shared_preferences_foundation:
+    :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
+  sqflite_darwin:
+    :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
+
+SPEC CHECKSUMS:
+  flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
+  FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
+  package_info_plus: 122abb51244f66eead59ce7c9c200d6b53111779
+  shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+  sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
+
+PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
+
+COCOAPODS: 1.16.2
diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj
index d11c248..18540de 100644
--- a/macos/Runner.xcodeproj/project.pbxproj
+++ b/macos/Runner.xcodeproj/project.pbxproj
@@ -21,12 +21,14 @@
 /* End PBXAggregateTarget section */
 
 /* Begin PBXBuildFile section */
+		20FED99443D842721917B5EA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11BF9FD2E5D40BBC266BE01E /* Pods_Runner.framework */; };
 		331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
 		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
 		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
 		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
 		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
 		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+		B4CD7DDD1DFB195BBB89E7B6 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C132C305A1AF3E6388C6B7C2 /* Pods_RunnerTests.framework */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -60,11 +62,12 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		11BF9FD2E5D40BBC266BE01E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
 		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
 		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
-		33CC10ED2044A3C60003C045 /* freegosy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "freegosy.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		33CC10ED2044A3C60003C045 /* freegosy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = freegosy.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
 		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -76,8 +79,15 @@
 		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
 		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
 		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		3FB6F37F00E4A78247BF6B40 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+		645F69065E0F4B8AB271256A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
+		7F03BA3B183B04F51E8CC981 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		81CF0E3069A0EAB2590738F9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+		C132C305A1AF3E6388C6B7C2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		CCAA1F871C96E6DDC1498078 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
+		EEECA80AC0A0FD92A265CAB6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -85,6 +95,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				B4CD7DDD1DFB195BBB89E7B6 /* Pods_RunnerTests.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -92,6 +103,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				20FED99443D842721917B5EA /* Pods_Runner.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -125,6 +137,7 @@
 				331C80D6294CF71000263BE5 /* RunnerTests */,
 				33CC10EE2044A3C60003C045 /* Products */,
 				D73912EC22F37F3D000D13A0 /* Frameworks */,
+				7236E435FDF89CA91259A96E /* Pods */,
 			);
 			sourceTree = "<group>";
 		};
@@ -172,9 +185,24 @@
 			path = Runner;
 			sourceTree = "<group>";
 		};
+		7236E435FDF89CA91259A96E /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				7F03BA3B183B04F51E8CC981 /* Pods-Runner.debug.xcconfig */,
+				3FB6F37F00E4A78247BF6B40 /* Pods-Runner.release.xcconfig */,
+				81CF0E3069A0EAB2590738F9 /* Pods-Runner.profile.xcconfig */,
+				645F69065E0F4B8AB271256A /* Pods-RunnerTests.debug.xcconfig */,
+				EEECA80AC0A0FD92A265CAB6 /* Pods-RunnerTests.release.xcconfig */,
+				CCAA1F871C96E6DDC1498078 /* Pods-RunnerTests.profile.xcconfig */,
+			);
+			path = Pods;
+			sourceTree = "<group>";
+		};
 		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				11BF9FD2E5D40BBC266BE01E /* Pods_Runner.framework */,
+				C132C305A1AF3E6388C6B7C2 /* Pods_RunnerTests.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -186,6 +214,7 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
 			buildPhases = (
+				1C4411D508300D115841BAD7 /* [CP] Check Pods Manifest.lock */,
 				331C80D1294CF70F00263BE5 /* Sources */,
 				331C80D2294CF70F00263BE5 /* Frameworks */,
 				331C80D3294CF70F00263BE5 /* Resources */,
@@ -204,11 +233,13 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
 			buildPhases = (
+				5C47E23C8AFB35CF3ADC0D54 /* [CP] Check Pods Manifest.lock */,
 				33CC10E92044A3C60003C045 /* Sources */,
 				33CC10EA2044A3C60003C045 /* Frameworks */,
 				33CC10EB2044A3C60003C045 /* Resources */,
 				33CC110E2044A8840003C045 /* Bundle Framework */,
 				3399D490228B24CF009A79C7 /* ShellScript */,
+				CBBB3DE13D8D81E5177BF4F9 /* [CP] Embed Pods Frameworks */,
 			);
 			buildRules = (
 			);
@@ -291,6 +322,28 @@
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
+		1C4411D508300D115841BAD7 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
 		3399D490228B24CF009A79C7 /* ShellScript */ = {
 			isa = PBXShellScriptBuildPhase;
 			alwaysOutOfDate = 1;
@@ -329,6 +382,45 @@
 			shellPath = /bin/sh;
 			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
 		};
+		5C47E23C8AFB35CF3ADC0D54 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		CBBB3DE13D8D81E5177BF4F9 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
@@ -380,6 +472,7 @@
 /* Begin XCBuildConfiguration section */
 		331C80DB294CF71000263BE5 /* Debug */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReference = 645F69065E0F4B8AB271256A /* Pods-RunnerTests.debug.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CURRENT_PROJECT_VERSION = 1;
@@ -394,6 +487,7 @@
 		};
 		331C80DC294CF71000263BE5 /* Release */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReference = EEECA80AC0A0FD92A265CAB6 /* Pods-RunnerTests.release.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CURRENT_PROJECT_VERSION = 1;
@@ -408,6 +502,7 @@
 		};
 		331C80DD294CF71000263BE5 /* Profile */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReference = CCAA1F871C96E6DDC1498078 /* Pods-RunnerTests.profile.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CURRENT_PROJECT_VERSION = 1;
@@ -476,8 +571,10 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = FWFZZNYS2X;
 				INFOPLIST_FILE = Runner/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -608,8 +705,10 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = FWFZZNYS2X;
 				INFOPLIST_FILE = Runner/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -628,8 +727,10 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = FWFZZNYS2X;
 				INFOPLIST_FILE = Runner/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata
index 1d526a1..21a3cc1 100644
--- a/macos/Runner.xcworkspace/contents.xcworkspacedata
+++ b/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,7 @@
    <FileRef
       location = "group:Runner.xcodeproj">
    </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
 </Workspace>
diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements
index dddb8a3..775b708 100644
--- a/macos/Runner/DebugProfile.entitlements
+++ b/macos/Runner/DebugProfile.entitlements
@@ -2,11 +2,17 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
-	<key>com.apple.security.cs.allow-jit</key>
-	<true/>
-	<key>com.apple.security.network.server</key>
-	<true/>
+    <key>com.apple.security.app-sandbox</key>
+    <false/>
+    <key>com.apple.security.cs.allow-jit</key>
+    <true/>
+    <key>com.apple.security.network.client</key>
+    <true/>
+    <key>com.apple.security.network.server</key>
+    <true/>
+    <key>keychain-access-groups</key>
+    <array>
+        <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+    </array>
 </dict>
 </plist>
diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements
index 852fa1a..67ffb8f 100644
--- a/macos/Runner/Release.entitlements
+++ b/macos/Runner/Release.entitlements
@@ -2,7 +2,19 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
+    <key>com.apple.security.app-sandbox</key>
+    <true/>
+    <key>com.apple.security.network.client</key>
+    <true/>
+    <key>com.apple.security.files.user-selected.read-write</key>
+    <true/>
+    <key>com.apple.security.files.downloads.read-write</key>
+    <true/>
+    <key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
+    <string>/</string>
+    <key>keychain-access-groups</key>
+    <array>
+        <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+    </array>
 </dict>
 </plist>

Clone this wiki locally