Skip to content

Navigation Menu

Sign in
Sign up

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

AltStyle によって変換されたページ (->オリジナル) /