├── .github ├── PULL_REQUEST_TEMPLATE │ └── fix-analyze-warnings.md └── workflows │ ├── README.md │ ├── build-and-release.yml │ ├── ci-build.yml │ ├── deploy-website.yml │ └── version_check.yml ├── .gitignore ├── .metadata ├── .vscode └── settings.json ├── Makefile ├── README.md ├── Screenshots ├── Screenshot_20250327_181806.png ├── Screenshot_20250327_181821.png └── Screenshot_20250327_181841.png ├── analysis_options.yaml ├── appimage ├── AppDir │ ├── .DirIcon │ ├── AppRun │ ├── usr │ │ ├── bin │ │ │ ├── data │ │ │ │ ├── flutter_assets │ │ │ │ │ ├── AssetManifest.bin │ │ │ │ │ ├── AssetManifest.json │ │ │ │ │ ├── FontManifest.json │ │ │ │ │ ├── NOTICES.Z │ │ │ │ │ ├── NativeAssetsManifest.json │ │ │ │ │ ├── fonts │ │ │ │ │ │ └── MaterialIcons-Regular.otf │ │ │ │ │ ├── shaders │ │ │ │ │ │ └── ink_sparkle.frag │ │ │ │ │ └── version.json │ │ │ │ └── icudtl.dat │ │ │ ├── lib │ │ │ │ ├── libapp.so │ │ │ │ ├── libfile_saver_plugin.so │ │ │ │ ├── libflutter_linux_gtk.so │ │ │ │ ├── libscreen_retriever_linux_plugin.so │ │ │ │ ├── liburl_launcher_linux_plugin.so │ │ │ │ └── libwindow_manager_plugin.so │ │ │ └── wine_prefix_manager │ │ └── share │ │ │ ├── applications │ │ │ └── wine_prefix_manager.desktop │ │ │ └── icons │ │ │ └── hicolor │ │ │ └── 256x256 │ │ │ └── apps │ │ │ └── wine_prefix_manager.png │ ├── wine_prefix_manager.desktop │ └── wine_prefix_manager.png └── WinePrefixManager.AppDir │ ├── .DirIcon │ ├── AppRun │ ├── wine_prefix_manager.desktop │ └── wine_prefix_manager.png ├── assets └── icons │ └── winehero.jpg ├── env.example ├── lib ├── config │ └── api_keys.dart ├── main.dart ├── models │ ├── build_models.dart │ ├── common_models.dart │ ├── igdb_models.dart │ ├── prefix_models.dart │ ├── settings.dart │ └── wine_build.dart ├── pages │ ├── backup_manager_page.dart │ ├── create_prefix_page.dart │ ├── file_manager_page.dart │ ├── files_and_backup_page.dart │ ├── game_details_page.dart │ ├── game_library_page.dart │ ├── home_page.dart │ ├── iso_mounting_page.dart │ ├── logs_page.dart │ ├── manage_prefixes_page.dart │ └── settings_page.dart ├── providers │ ├── prefix_provider.dart │ ├── settings_provider.dart │ └── window_control_provider.dart ├── services │ ├── backup_service.dart │ ├── build_service.dart │ ├── compressed_game_service.dart │ ├── cover_art_service.dart │ ├── igdb_service.dart │ ├── iso_mounting_service.dart │ ├── log_service.dart │ ├── power_management_service.dart │ ├── prefix_creation_service.dart │ ├── prefix_creator.dart │ ├── prefix_management_service.dart │ ├── prefix_storage_service.dart │ ├── process_service.dart │ ├── proton_prefix_creation_service.dart │ ├── ui_action_service.dart │ ├── wine_component_installer.dart │ ├── wine_prefix_creation_service.dart │ └── winecfg_32bit_helper.dart ├── theme │ └── theme_provider.dart ├── utils │ └── path_utils.dart └── widgets │ ├── about_screen.dart │ ├── action_button.dart │ ├── change_prefix_dialog.dart │ ├── common_components_dialog.dart │ ├── custom_title_bar.dart │ ├── env_variables_dialog.dart │ ├── executable_list_tile.dart │ ├── game_card.dart │ ├── game_carousel.dart │ ├── game_details_dialog.dart │ ├── game_info_modal.dart │ ├── game_search_dialog.dart │ ├── game_settings_modal.dart │ ├── json_viewer_page.dart │ ├── prefix_creation_form.dart │ ├── prefix_creation_form_for_type.dart │ ├── prefix_detail_actions.dart │ ├── prefix_list_tile.dart │ ├── rename_prefix_dialog.dart │ ├── text_input_dialog.dart │ ├── window_buttons.dart │ └── wine_component_settings_dialog.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── my_application.cc └── runner │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── pubspec.lock ├── pubspec.yaml ├── scripts ├── build_appimage.sh ├── check_version_sync.sh ├── create_github_release.sh └── setup_iso_mounting.sh ├── website ├── assets │ └── favicon.svg ├── css │ ├── style.css │ └── twitch-setup.css ├── debug.html ├── images │ ├── about.png │ ├── backup-creator.png │ ├── backup-manager.png │ ├── gamelibrary.png │ ├── hero-screenshot.png │ ├── logs.png │ ├── prefixcreator.png │ ├── prefixmanager.png │ └── protonprefixes.png ├── index.html ├── js │ ├── script.js │ └── twitch-setup.js └── serve.sh └── wine_prefix_manager.desktop /.github/PULL_REQUEST_TEMPLATE/fix-analyze-warnings.md: -------------------------------------------------------------------------------- 1 | # Fixing Other Flutter Analyze Warnings 2 | 3 | This PR addresses various warnings and issues detected by Flutter analyze: 4 | 5 | 1. **Remove unused imports**: 6 | - Remove unused imports from lib/main.dart, lib/pages/create_prefix_page.dart, and other files 7 | 8 | 2. **Fix BuildContext usage in async gaps**: 9 | - Add mounted checks or refactor code to avoid BuildContext usage across async gaps 10 | 11 | 3. **Add const for constructors**: 12 | - Update widget constructors to use const where possible for better performance 13 | 14 | 4. **Update deprecated API usage**: 15 | - Replace withOpacity() with withValues() 16 | - Replace background with surface 17 | - Replace surfaceVariant with surfaceContainerHighest 18 | 19 | 5. **Fix immutable warnings**: 20 | - Make GameDetailsDialog.settings final 21 | 22 | 6. **Remove unused fields and variables**: 23 | - Remove _navigatorKey and other unused fields 24 | - Remove downloadedCovers and other unused local variables 25 | 26 | 7. **Fix unnecessary null comparison**: 27 | - Fix unnecessary null comparison in prefix_management_service.dart 28 | 29 | 8. **Fix unnecessary string interpolation**: 30 | - Remove unnecessary braces in string interpolations 31 | 32 | These changes will improve code quality, reduce warnings, and make the codebase more maintainable. 33 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions for Wine Prefix Manager 2 | 3 | This repository includes GitHub Actions workflows for building and releasing the Wine Prefix Manager application. 4 | 5 | ## Available Workflows 6 | 7 | ### CI Build 8 | The CI workflow runs on every push to the main branch and on pull requests. It performs: 9 | - Code analysis 10 | - Debug build for Linux 11 | 12 | ### Build and Release 13 | The release workflow builds and packages Wine Prefix Manager for Linux in multiple formats: 14 | - Tar.gz archive 15 | - Debian (.deb) package 16 | - RPM (.rpm) package 17 | - AppImage 18 | 19 | ## Triggering Builds 20 | 21 | ### Manual Builds 22 | You can manually trigger a build from the GitHub Actions tab: 23 | 1. Navigate to the "Actions" tab in your repository 24 | 2. Select "Build and Release" workflow 25 | 3. Click "Run workflow" 26 | 4. Enter the version number (e.g., 1.9.1) 27 | 5. Click "Run workflow" 28 | 29 | ### Automated Releases 30 | To create an official release: 31 | 1. Create and push a git tag with a version number: 32 | ```bash 33 | git tag -a v1.9.2 -m "Release v1.9.2" 34 | git push origin v1.9.2 35 | ``` 36 | 37 | 2. The Build and Release workflow will automatically run, creating all package formats. 38 | 3. A GitHub Release will be created with all the build artifacts. 39 | 40 | ## Package Requirements 41 | 42 | - The .deb package requires Wine to be installed 43 | - The .rpm package requires Wine to be installed 44 | - The AppImage bundles most dependencies, but still requires Wine to be installed 45 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | paths-ignore: 9 | - '**.md' 10 | - 'docs/**' 11 | - '.github/ISSUE_TEMPLATE/**' 12 | 13 | jobs: 14 | build-linux: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Flutter 21 | uses: subosito/flutter-action@v2 22 | with: 23 | channel: 'stable' 24 | flutter-version: '3.x' 25 | 26 | - name: Install Linux build dependencies 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get install -y cmake ninja-build clang pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev 30 | 31 | - name: Install Flutter dependencies 32 | run: flutter pub get 33 | 34 | - name: Analyze project source 35 | run: | 36 | # Create a temporary analysis options file to ignore plugin warnings 37 | cat > .analyze_temp_options.yaml << EOL 38 | include: package:flutter_lints/flutter.yaml 39 | 40 | analyzer: 41 | errors: 42 | invalid_use_of_internal_member: ignore 43 | missing_required_param: error 44 | missing_return: error 45 | must_be_immutable: warning 46 | sort_child_properties_last: ignore 47 | deprecated_member_use: ignore 48 | use_build_context_synchronously: ignore 49 | unnecessary_null_comparison: ignore 50 | unused_import: ignore 51 | unused_field: ignore 52 | unused_local_variable: ignore 53 | unused_element: ignore 54 | unnecessary_import: ignore 55 | exclude: 56 | - "**/*.g.dart" 57 | - "**/*.freezed.dart" 58 | - "lib/generated_plugin_registrant.dart" 59 | EOL 60 | 61 | # Run analyze with our temporary options file 62 | RESULT=0 63 | flutter analyze --no-fatal-warnings --options=.analyze_temp_options.yaml || RESULT=$? 64 | 65 | # Check if there are any real errors that aren't from file_picker 66 | flutter analyze --options=.analyze_temp_options.yaml 2>&1 | grep -v "file_picker" | grep -E "^error" > real_errors.txt || true 67 | 68 | if [ -s real_errors.txt ]; then 69 | echo "Critical errors found in your code that must be fixed before merging:" 70 | cat real_errors.txt 71 | exit 1 72 | else 73 | echo "No critical issues detected, proceeding with build" 74 | fi 75 | 76 | # Clean up temp file 77 | rm -f .analyze_temp_options.yaml real_errors.txt 78 | 79 | - name: Build Linux debug application 80 | run: flutter build linux --debug 81 | 82 | - name: Archive build output 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: linux-debug-build 86 | path: build/linux/x64/debug/bundle 87 | retention-days: 1 88 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: [ 'website/**' ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup Pages 29 | uses: actions/configure-pages@v4 30 | 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v3 33 | with: 34 | path: './website' 35 | 36 | - name: Deploy to GitHub Pages 37 | id: deployment 38 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/version_check.yml: -------------------------------------------------------------------------------- 1 | name: Version Sync Check 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | version_check: 12 | name: Check Version Synchronization 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 # Fetch all history for tags 20 | 21 | - name: Set up permissions 22 | run: chmod +x scripts/check_version_sync.sh 23 | 24 | - name: Check version sync 25 | id: version_check 26 | run: | 27 | # Run the version check script 28 | if ./scripts/check_version_sync.sh; then 29 | echo "status=synced" >> $GITHUB_OUTPUT 30 | echo "✅ Versions are in sync" 31 | else 32 | exit_code=$? 33 | if [ $exit_code -eq 1 ]; then 34 | echo "status=out_of_sync" >> $GITHUB_OUTPUT 35 | echo "❌ Versions are out of sync" 36 | else 37 | echo "status=error" >> $GITHUB_OUTPUT 38 | echo "💥 Error occurred during version check" 39 | fi 40 | exit $exit_code 41 | fi 42 | 43 | - name: Get pubspec version 44 | id: pubspec_version 45 | run: | 46 | version=$(grep "^version:" pubspec.yaml | sed 's/version: *//g' | sed 's/ *#.*//g' | tr -d '"' | tr -d "'") 47 | echo "version=$version" >> $GITHUB_OUTPUT 48 | echo "Pubspec version: $version" 49 | 50 | - name: Get latest tag 51 | id: latest_tag 52 | run: | 53 | latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "none") 54 | echo "tag=$latest_tag" >> $GITHUB_OUTPUT 55 | echo "Latest tag: $latest_tag" 56 | 57 | - name: Comment on PR (if out of sync) 58 | if: github.event_name == 'pull_request' && steps.version_check.outputs.status == 'out_of_sync' 59 | uses: actions/github-script@v7 60 | with: 61 | script: | 62 | const pubspecVersion = '${{ steps.pubspec_version.outputs.version }}'; 63 | const latestTag = '${{ steps.latest_tag.outputs.tag }}'; 64 | 65 | const comment = `## ⚠️ Version Synchronization Issue 66 | 67 | The version in \`pubspec.yaml\` does not match the latest git tag: 68 | 69 | - **Pubspec version:** \`${pubspecVersion}\` 70 | - **Latest git tag:** \`${latestTag}\` 71 | 72 | ### Actions needed: 73 | 1. **Option A:** Update \`pubspec.yaml\` version to match the tag: \`${latestTag.replace(/^v/, '')}\` 74 | 2. **Option B:** Create a new tag for the current version: 75 | \`\`\`bash 76 | git tag -a v${pubspecVersion.replace(/\+.*/, '')} -m "Release version ${pubspecVersion.replace(/\+.*/, '')}" 77 | git push origin v${pubspecVersion.replace(/\+.*/, '')} 78 | \`\`\` 79 | 80 | This check ensures version consistency across your project.`; 81 | 82 | github.rest.issues.createComment({ 83 | issue_number: context.issue.number, 84 | owner: context.repo.owner, 85 | repo: context.repo.repo, 86 | body: comment 87 | }); 88 | 89 | - name: Set job summary 90 | run: | 91 | pubspec_version="${{ steps.pubspec_version.outputs.version }}" 92 | latest_tag="${{ steps.latest_tag.outputs.tag }}" 93 | status="${{ steps.version_check.outputs.status }}" 94 | 95 | echo "# Version Sync Check Results" >> $GITHUB_STEP_SUMMARY 96 | echo "" >> $GITHUB_STEP_SUMMARY 97 | echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY 98 | echo "|------|-------|" >> $GITHUB_STEP_SUMMARY 99 | echo "| Pubspec Version | \`$pubspec_version\` |" >> $GITHUB_STEP_SUMMARY 100 | echo "| Latest Git Tag | \`$latest_tag\` |" >> $GITHUB_STEP_SUMMARY 101 | 102 | if [ "$status" = "synced" ]; then 103 | echo "| Status | ✅ **In Sync** |" >> $GITHUB_STEP_SUMMARY 104 | elif [ "$status" = "out_of_sync" ]; then 105 | echo "| Status | ❌ **Out of Sync** |" >> $GITHUB_STEP_SUMMARY 106 | else 107 | echo "| Status | ⚠️ **Error** |" >> $GITHUB_STEP_SUMMARY 108 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # Environment variables (contains sensitive API keys) 14 | .env 15 | .env.local 16 | .env.production 17 | .env.development 18 | 19 | # IntelliJ related 20 | *.iml 21 | *.ipr 22 | *.iws 23 | .idea/ 24 | 25 | # The .vscode folder contains launch configuration and tasks you configure in 26 | # VS Code which you may wish to be included in version control, so this line 27 | # is commented out by default. 28 | #.vscode/ 29 | 30 | # Flutter/Dart/Pub related 31 | **/doc/api/ 32 | **/ios/Flutter/flutter_assets/ 33 | **/ios/Flutter/flutter_export_environment.sh 34 | .dart_tool/ 35 | .flutter-plugins 36 | .flutter-plugins-dependencies 37 | .packages 38 | .pub-cache/ 39 | .pub/ 40 | /build/ 41 | 42 | # Symbolication related 43 | app.*.symbols 44 | 45 | # Obfuscation related 46 | app.*.map.json 47 | 48 | # Android Studio will place build artifacts here 49 | /android/app/debug 50 | /android/app/profile 51 | /android/app/release 52 | 53 | # Wine builds and installations (should not be in source control) 54 | wine_builds/ 55 | **/wine_builds/ 56 | proton_builds/ 57 | **/proton_builds/ 58 | .wine_prefixes/ 59 | **/wine_prefixes/ 60 | 61 | # Build and release artifacts (AppImages should only exist in GitHub releases) 62 | *.AppImage 63 | appimage_temp/ 64 | appimage/ 65 | **/appimage/ 66 | *.tar.gz 67 | *.tar.xz 68 | *.zip 69 | *.deb 70 | *.rpm 71 | 72 | # Wine build archives and extracted files 73 | GE-Proton*.tar.gz 74 | wine-*.tar.xz 75 | wine-proton-*.tar.xz 76 | 77 | # Development files 78 | *.tmp 79 | *.temp 80 | .cache/ 81 | 82 | # Build artifacts and releases 83 | *.AppImage 84 | *.tar.gz 85 | *.zip 86 | *.deb 87 | *.rpm 88 | release/ 89 | dist/ 90 | appimage/ 91 | 92 | # Temporary files 93 | *.tmp 94 | *.temp 95 | *~ 96 | .#* 97 | 98 | # OS generated files 99 | Thumbs.db 100 | ehthumbs.db 101 | Desktop.ini 102 | $RECYCLE.BIN/ 103 | 104 | # Archive files (unless specifically needed) 105 | *.7z 106 | *.dmg 107 | *.gz 108 | *.iso 109 | *.jar 110 | *.rar 111 | *.tar 112 | *.zip 113 | 114 | # Logs 115 | *.log 116 | logs/ 117 | 118 | # Runtime data 119 | pids 120 | *.pid 121 | *.seed 122 | *.pid.lock 123 | 124 | # Coverage directory used by tools like istanbul 125 | coverage/ 126 | 127 | # nyc test coverage 128 | .nyc_output 129 | 130 | # Dependency directories 131 | node_modules/ 132 | 133 | # Optional npm cache directory 134 | .npm 135 | 136 | # Optional REPL history 137 | .node_repl_history 138 | 139 | # Output of 'npm pack' 140 | *.tgz 141 | 142 | # Yarn Integrity file 143 | .yarn-integrity 144 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "archlinuxaur0000000000000000000000000000" 8 | channel: "" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: archlinuxaur0000000000000000000000000000 17 | base_revision: archlinuxaur0000000000000000000000000000 18 | - platform: android 19 | create_revision: archlinuxaur0000000000000000000000000000 20 | base_revision: archlinuxaur0000000000000000000000000000 21 | - platform: ios 22 | create_revision: archlinuxaur0000000000000000000000000000 23 | base_revision: archlinuxaur0000000000000000000000000000 24 | - platform: linux 25 | create_revision: archlinuxaur0000000000000000000000000000 26 | base_revision: archlinuxaur0000000000000000000000000000 27 | - platform: macos 28 | create_revision: archlinuxaur0000000000000000000000000000 29 | base_revision: archlinuxaur0000000000000000000000000000 30 | - platform: web 31 | create_revision: archlinuxaur0000000000000000000000000000 32 | base_revision: archlinuxaur0000000000000000000000000000 33 | - platform: windows 34 | create_revision: archlinuxaur0000000000000000000000000000 35 | base_revision: archlinuxaur0000000000000000000000000000 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.sourceDirectory": "/home/jon/wine_prefix_manager/linux" 3 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Wine Prefix Manager - Makefile 2 | 3 | .PHONY: help version-check version-fix version-info build clean test format lint install appimage github-release 4 | 5 | # Default target 6 | help: 7 | @echo "Wine Prefix Manager - Available commands:" 8 | @echo "" 9 | @echo "Version Management:" 10 | @echo " version-check Check if pubspec.yaml version matches git tags" 11 | @echo " version-fix Auto-create git tag to match pubspec.yaml version" 12 | @echo " version-info Show current version information" 13 | @echo "" 14 | @echo "Development:" 15 | @echo " build Build the Flutter application" 16 | @echo " clean Clean build artifacts" 17 | @echo " test Run tests" 18 | @echo " format Format Dart code" 19 | @echo " lint Run linter (dart analyze)" 20 | @echo " install Get Flutter dependencies" 21 | @echo "" 22 | @echo "Distribution:" 23 | @echo " appimage Build AppImage for distribution" 24 | @echo " github-release Create GitHub release with AppImage (requires VERSION=x.y.z)" 25 | @echo " release-all Complete release workflow (tag + AppImage + GitHub)" 26 | @echo "" 27 | @echo "Release Management:" 28 | @echo " tag-release Create and push a release tag (requires VERSION=x.y.z)" 29 | @echo "" 30 | 31 | # Version management targets 32 | version-check: 33 | @echo "🔍 Checking version synchronization..." 34 | @./scripts/check_version_sync.sh 35 | 36 | version-fix: 37 | @echo "🔧 Auto-fixing version synchronization..." 38 | @./scripts/check_version_sync.sh --fix 39 | 40 | version-info: 41 | @echo "📋 Current version information:" 42 | @echo " Pubspec version: $$(grep '^version:' pubspec.yaml | sed 's/version: *//g' | tr -d '"' | tr -d "'")" 43 | @echo " Latest git tag: $$(git describe --tags --abbrev=0 2>/dev/null || echo 'none')" 44 | @echo " Current commit: $$(git rev-parse --short HEAD 2>/dev/null || echo 'not a git repo')" 45 | @echo " Branch: $$(git branch --show-current 2>/dev/null || echo 'not a git repo')" 46 | 47 | # Flutter development targets 48 | build: 49 | @echo "🏗️ Building Flutter application..." 50 | @flutter build linux 51 | 52 | clean: 53 | @echo "🧹 Cleaning build artifacts..." 54 | @flutter clean 55 | @rm -rf appimage/ 56 | 57 | test: 58 | @echo "🧪 Running tests..." 59 | @flutter test 60 | 61 | format: 62 | @echo "✨ Formatting Dart code..." 63 | @dart format . 64 | 65 | lint: 66 | @echo "🔍 Running linter..." 67 | @dart analyze 68 | 69 | install: 70 | @echo "📦 Getting Flutter dependencies..." 71 | @flutter pub get 72 | 73 | # Distribution targets 74 | appimage: 75 | @echo "📦 Building AppImage..." 76 | @chmod +x scripts/build_appimage.sh 77 | @./scripts/build_appimage.sh 78 | 79 | github-release: 80 | ifndef VERSION 81 | $(error VERSION is not set. Usage: make github-release VERSION=1.0.0) 82 | endif 83 | @echo "🚀 Creating GitHub release v$(VERSION)..." 84 | @chmod +x scripts/create_github_release.sh 85 | @./scripts/create_github_release.sh --version $(VERSION) 86 | 87 | # Release management 88 | tag-release: 89 | ifndef VERSION 90 | $(error VERSION is not set. Usage: make tag-release VERSION=1.0.0) 91 | endif 92 | @echo "🚀 Creating release tag v$(VERSION)..." 93 | @echo "1. Updating pubspec.yaml version..." 94 | @sed -i 's/^version: .*/version: $(VERSION)/' pubspec.yaml 95 | @echo "2. Committing version update..." 96 | @git add pubspec.yaml 97 | @git commit -m "chore: bump version to $(VERSION)" || echo "No changes to commit" 98 | @echo "3. Creating git tag..." 99 | @git tag -a v$(VERSION) -m "Release version $(VERSION)" 100 | @echo "4. Pushing changes and tag..." 101 | @git push origin $$(git branch --show-current) 102 | @git push origin v$(VERSION) 103 | @echo "✅ Release tag v$(VERSION) created and pushed!" 104 | 105 | # Complete release workflow 106 | release-all: 107 | ifndef VERSION 108 | $(error VERSION is not set. Usage: make release-all VERSION=1.0.0) 109 | endif 110 | @echo "🚀 Starting complete release workflow for v$(VERSION)..." 111 | @echo "" 112 | @echo "📋 Step 1/4: Creating git tag and pushing..." 113 | @make tag-release VERSION=$(VERSION) 114 | @echo "" 115 | @echo "📋 Step 2/4: Building AppImage..." 116 | @make appimage 117 | @echo "" 118 | @echo "📋 Step 3/4: Creating GitHub release..." 119 | @make github-release VERSION=$(VERSION) 120 | @echo "" 121 | @echo "📋 Step 4/4: Final verification..." 122 | @make version-check 123 | @echo "" 124 | @echo "✅ Complete release v$(VERSION) finished!" 125 | @echo "🎉 Your release is now live on GitHub with AppImage attached!" 126 | @echo "" 127 | @echo "📁 AppImage location: ./appimage/WinePrefixManager-$(VERSION)-x86_64.AppImage" 128 | @echo "🔗 Release URL: https://github.com/CrownParkComputing/wine_prefix_manager/releases/tag/v$(VERSION)" 129 | 130 | # Check if Flutter is installed 131 | check-flutter: 132 | @which flutter > /dev/null || (echo "❌ Flutter not found. Please install Flutter first." && exit 1) 133 | @echo "✅ Flutter is installed: $$(flutter --version | head -n 1)" 134 | 135 | # Development setup 136 | setup: check-flutter install 137 | @echo "🔧 Setting up development environment..." 138 | @chmod +x scripts/check_version_sync.sh 139 | @chmod +x scripts/build_appimage.sh 140 | @chmod +x scripts/create_github_release.sh 141 | @echo "✅ Development environment ready!" 142 | 143 | # Pre-commit checks 144 | pre-commit: format lint test version-check 145 | @echo "✅ All pre-commit checks passed!" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wine Prefix Manager 2 | 3 | A modern Flutter application for managing Wine/Proton prefixes on Linux with comprehensive gaming features, ISO mounting, and built-in IGDB integration. 4 | 5 | ![Version](https://img.shields.io/badge/version-3.3.1-blue) 6 | ![Platform](https://img.shields.io/badge/platform-Linux-green) 7 | ![License](https://img.shields.io/badge/license-MIT-red) 8 | 9 | ## ✨ Features 10 | 11 | ### 🎮 Game Library Management 12 | - **Visual Game Library** - Beautiful grid view with cover art and metadata 13 | - **IGDB Integration** - Automatic game metadata, cover art, and screenshots 14 | - **Game Categories** - Organize your games with custom categories 15 | - **Advanced Search** - Find games quickly with powerful search functionality 16 | - **Game Details** - Comprehensive game information and launch options 17 | 18 | ### 🍷 Wine Prefix Management 19 | - **Modern Flutter UI** - Beautiful, responsive interface with dark/light themes 20 | - **Comprehensive Prefix Management** - Create, manage, and organize Wine/Proton prefixes 21 | - **Proton Integration** - Seamless Steam Proton support with automatic detection 22 | - **32-bit & 64-bit Support** - Full support for both architectures 23 | - **Automatic Setup** - Microsoft Visual C++ redistributables and gaming dependencies 24 | 25 | ### 💿 ISO/CD Management 26 | - **Virtual CD Drive Simulation** - Mount ISO files as real CD drives (D:) in Wine prefixes 27 | - **Multiple Format Support** - .iso, .img, .bin, .cue files 28 | - **Real-time Status** - Live updates during mounting/unmounting operations 29 | - **Automatic Wine Registry Configuration** - Proper CD-ROM drive detection 30 | - **Safe Ejection** - Clean unmounting with automatic cleanup 31 | 32 | ### 💾 Backup & Restore 33 | - **Complete Backup System** - Backup entire prefixes with games and settings 34 | - **Selective Restore** - Restore specific components or complete prefixes 35 | - **Compressed Archives** - Efficient storage with compression 36 | - **Backup Scheduling** - Automatic backup reminders and management 37 | 38 | ### 📁 File Management 39 | - **Integrated File Browser** - Browse and manage prefix files directly 40 | - **Quick Access** - Jump to common directories (Program Files, System32, etc.) 41 | - **File Operations** - Copy, move, delete files within prefixes 42 | - **Registry Editing** - Access to Wine registry for advanced configuration 43 | 44 | ### ⚙️ Advanced Configuration 45 | - **Environment Variables** - Manage prefix-specific environment variables 46 | - **Controller Support** - Built-in controller fixes for gaming 47 | - **Wine Version Management** - Switch between different Wine versions 48 | - **Performance Optimization** - DXVK, VKD3D integration for 64-bit prefixes 49 | 50 | ### 🔧 System Integration 51 | - **One-Click AppImage** - Portable, no-installation-required distribution 52 | - **Desktop Integration** - .desktop file for system menu integration 53 | - **Logging System** - Comprehensive logging for troubleshooting 54 | - **Power Management** - Prevent system sleep during long operations 55 | 56 | ## 🚀 Quick Start 57 | 58 | ### Download & Install 59 | 60 | 1. **Download the AppImage** from [Releases](https://github.com/CrownParkComputing/wine_prefix_manager/releases/latest) 61 | 2. **Make it executable:** 62 | ```bash 63 | chmod +x WinePrefixManager-*.AppImage 64 | ``` 65 | 3. **Run the application:** 66 | ```bash 67 | ./WinePrefixManager-*.AppImage 68 | ``` 69 | 70 | ### First Launch Setup 71 | 72 | 1. **Configure Prefix Directory** - Choose where to store your Wine prefixes 73 | 2. **Set Game Library Path** - Select your games directory for automatic detection 74 | 3. **Install ISO Mounting** (Optional) - Run setup for password-free ISO mounting: 75 | ```bash 76 | ./scripts/setup_iso_mounting.sh 77 | ``` 78 | 79 | ## 🎮 Using the Application 80 | 81 | ### Creating Wine Prefixes 82 | 1. Navigate to **Manage Prefixes** tab 83 | 2. Click **Create New Prefix** 84 | 3. Choose Wine/Proton version and architecture 85 | 4. Configure initial settings and dependencies 86 | 5. Wait for automatic setup completion 87 | 88 | ### Managing Your Game Library 89 | 1. Go to **Game Library** tab 90 | 2. Add games manually or let the app auto-detect 91 | 3. Fetch metadata from IGDB for cover art and details 92 | 4. Organize games into categories 93 | 5. Launch games directly from the library 94 | 95 | ### Mounting ISO Files 96 | 1. Open **ISO/CD** tab 97 | 2. Select target Wine prefix 98 | 3. Click **Mount ISO** and choose your file 99 | 4. The ISO appears as drive D: in the selected prefix 100 | 5. Eject safely when done 101 | 102 | ### Backup & Restore 103 | 1. Access **Files & Backup** tab 104 | 2. Select prefixes to backup 105 | 3. Choose backup location and options 106 | 4. Monitor backup progress 107 | 5. Restore from backups when needed 108 | 109 | ## 🔧 Development 110 | 111 | ### Prerequisites 112 | - Flutter SDK (latest stable) with Linux desktop support 113 | - Wine or Proton installed 114 | - Git 115 | 116 | ### Building from Source 117 | 118 | 1. **Clone and setup:** 119 | ```bash 120 | git clone https://github.com/CrownParkComputing/wine_prefix_manager.git 121 | cd wine_prefix_manager 122 | make setup 123 | ``` 124 | 125 | 2. **Development build:** 126 | ```bash 127 | make build 128 | flutter run -d linux 129 | ``` 130 | 131 | ### Available Commands 132 | ```bash 133 | # Development 134 | make setup # Setup development environment 135 | make build # Build the application 136 | make test # Run tests 137 | make lint # Run linter 138 | make format # Format code 139 | make clean # Clean build artifacts 140 | 141 | # Distribution 142 | make appimage # Build AppImage 143 | make release-all VERSION=x.y.z # Complete release workflow 144 | 145 | # Version Management 146 | make version-check # Check version synchronization 147 | make version-fix # Auto-fix version issues 148 | ``` 149 | 150 | ### IGDB Integration Setup (Developers) 151 | 152 | For developers building from source: 153 | 154 | 1. **Get IGDB credentials** from [Twitch Developer Console](https://dev.twitch.tv/console/apps) 155 | 2. **Copy environment template:** 156 | ```bash 157 | cp env.example .env 158 | ``` 159 | 3. **Add credentials to .env:** 160 | ```env 161 | IGDB_CLIENT_ID=your_client_id_here 162 | IGDB_CLIENT_SECRET=your_client_secret_here 163 | ``` 164 | 165 | ## 📁 Project Structure 166 | 167 | ``` 168 | wine_prefix_manager/ 169 | ├── lib/ # Flutter source code 170 | │ ├── pages/ # Application pages/screens 171 | │ ├── services/ # Core business logic services 172 | │ ├── widgets/ # Reusable UI components 173 | │ ├── models/ # Data models 174 | │ ├── providers/ # State management 175 | │ └── theme/ # UI theming 176 | ├── scripts/ # Build and utility scripts 177 | │ ├── build_appimage.sh # AppImage creation 178 | │ ├── create_github_release.sh # GitHub release automation 179 | │ ├── check_version_sync.sh # Version management 180 | │ └── setup_iso_mounting.sh # ISO mounting setup 181 | ├── appimage/ # AppImage build artifacts 182 | ├── website/ # Project website 183 | ├── Makefile # Build orchestration 184 | ├── pubspec.yaml # Flutter dependencies 185 | └── env.example # Environment template 186 | ``` 187 | 188 | ## 🐛 Troubleshooting 189 | 190 | ### Common Issues 191 | 192 | 1. **AppImage won't run:** 193 | ```bash 194 | # Install FUSE if needed 195 | sudo apt install fuse # Ubuntu/Debian 196 | sudo pacman -S fuse # Arch Linux 197 | sudo dnf install fuse # Fedora 198 | ``` 199 | 200 | 2. **ISO mounting requires password:** 201 | ```bash 202 | # Run the setup script once 203 | sudo ./scripts/setup_iso_mounting.sh 204 | ``` 205 | 206 | 3. **Games won't launch:** 207 | - Check prefix configuration in Manage Prefixes 208 | - Verify Wine/Proton version compatibility 209 | - Check logs tab for detailed error messages 210 | 211 | 4. **IGDB integration not working:** 212 | - For users: Should work automatically 213 | - For developers: Check IGDB credentials in .env file 214 | 215 | ### Debug Information 216 | 217 | - **Logs**: Available in the "Logs" tab within the application 218 | - **Verbose logging**: Run with `flutter run -d linux --verbose` 219 | - **Log files**: Check application data directory for persistent logs 220 | 221 | ## 🤝 Contributing 222 | 223 | 1. Fork the repository 224 | 2. Create a feature branch: `git checkout -b feature/new-feature` 225 | 3. Make your changes 226 | 4. Run quality checks: `make pre-commit` 227 | 5. Commit changes: `git commit -m "Add new feature"` 228 | 6. Push to branch: `git push origin feature/new-feature` 229 | 7. Submit a pull request 230 | 231 | ## 📝 License 232 | 233 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 234 | 235 | ## 🌟 Support 236 | 237 | - **Issues**: [GitHub Issues](https://github.com/CrownParkComputing/wine_prefix_manager/issues) 238 | - **Discussions**: [GitHub Discussions](https://github.com/CrownParkComputing/wine_prefix_manager/discussions) 239 | - **Releases**: [GitHub Releases](https://github.com/CrownParkComputing/wine_prefix_manager/releases) 240 | 241 | --- 242 | 243 | **Made with ❤️ for the Linux gaming community** -------------------------------------------------------------------------------- /Screenshots/Screenshot_20250327_181806.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/Screenshots/Screenshot_20250327_181806.png -------------------------------------------------------------------------------- /Screenshots/Screenshot_20250327_181821.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/Screenshots/Screenshot_20250327_181821.png -------------------------------------------------------------------------------- /Screenshots/Screenshot_20250327_181841.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/Screenshots/Screenshot_20250327_181841.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | analyzer: 28 | errors: 29 | # Ignore the file_picker package warnings 30 | invalid_use_of_internal_member: ignore 31 | unnecessary_import: ignore 32 | implementation_imports: ignore 33 | exclude: 34 | - "**/*.g.dart" 35 | - "**/*.freezed.dart" 36 | - "lib/generated_plugin_registrant.dart" 37 | 38 | # Additional information about this file can be found at 39 | # https://dart.dev/guides/language/analysis-options 40 | -------------------------------------------------------------------------------- /appimage/AppDir/.DirIcon: -------------------------------------------------------------------------------- 1 | wine_prefix_manager.png -------------------------------------------------------------------------------- /appimage/AppDir/AppRun: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | HERE="$(dirname "$(readlink -f "${0}")")" 3 | export PATH="${HERE}/usr/bin:${PATH}" 4 | export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}" 5 | exec "${HERE}/usr/bin/wine_prefix_manager" "$@" 6 | -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/AssetManifest.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/AssetManifest.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/FontManifest.json: -------------------------------------------------------------------------------- 1 | [{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]}] -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/NOTICES.Z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/data/flutter_assets/NOTICES.Z -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/NativeAssetsManifest.json: -------------------------------------------------------------------------------- 1 | {"format-version":[1,0,0],"native-assets":{}} -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/fonts/MaterialIcons-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/data/flutter_assets/fonts/MaterialIcons-Regular.otf -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/shaders/ink_sparkle.frag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/data/flutter_assets/shaders/ink_sparkle.frag -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/flutter_assets/version.json: -------------------------------------------------------------------------------- 1 | {"app_name":"wine_prefix_manager","version":"3.2.0","build_number":"1748459016","package_name":"wine_prefix_manager"} -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/data/icudtl.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/data/icudtl.dat -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/lib/libapp.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/lib/libapp.so -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/lib/libfile_saver_plugin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/lib/libfile_saver_plugin.so -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/lib/libflutter_linux_gtk.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/lib/libflutter_linux_gtk.so -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/lib/libscreen_retriever_linux_plugin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/lib/libscreen_retriever_linux_plugin.so -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/lib/liburl_launcher_linux_plugin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/lib/liburl_launcher_linux_plugin.so -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/lib/libwindow_manager_plugin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/lib/libwindow_manager_plugin.so -------------------------------------------------------------------------------- /appimage/AppDir/usr/bin/wine_prefix_manager: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/bin/wine_prefix_manager -------------------------------------------------------------------------------- /appimage/AppDir/usr/share/applications/wine_prefix_manager.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Wine Prefix Manager 4 | Comment=Manage Wine prefixes with ease 5 | Exec=wine_prefix_manager 6 | Icon=wine_prefix_manager 7 | Categories=Utility;System; 8 | Terminal=false 9 | StartupWMClass=wine_prefix_manager 10 | -------------------------------------------------------------------------------- /appimage/AppDir/usr/share/icons/hicolor/256x256/apps/wine_prefix_manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/usr/share/icons/hicolor/256x256/apps/wine_prefix_manager.png -------------------------------------------------------------------------------- /appimage/AppDir/wine_prefix_manager.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Wine Prefix Manager 4 | Comment=Manage Wine prefixes with ease 5 | Exec=wine_prefix_manager 6 | Icon=wine_prefix_manager 7 | Categories=Utility;System; 8 | Terminal=false 9 | StartupWMClass=wine_prefix_manager 10 | -------------------------------------------------------------------------------- /appimage/AppDir/wine_prefix_manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/AppDir/wine_prefix_manager.png -------------------------------------------------------------------------------- /appimage/WinePrefixManager.AppDir/.DirIcon: -------------------------------------------------------------------------------- 1 | wine_prefix_manager.png -------------------------------------------------------------------------------- /appimage/WinePrefixManager.AppDir/AppRun: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APPDIR="$(dirname "$(readlink -f "$0")")" 3 | export PATH="$APPDIR:$PATH" 4 | exec "$APPDIR/wine_prefix_manager" "$@" 5 | -------------------------------------------------------------------------------- /appimage/WinePrefixManager.AppDir/wine_prefix_manager.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Wine Prefix Manager 4 | Comment=Manage Wine prefixes with ease 5 | Exec=wine_prefix_manager 6 | Icon=wine_prefix_manager 7 | Categories=System;Utility; 8 | Terminal=false 9 | -------------------------------------------------------------------------------- /appimage/WinePrefixManager.AppDir/wine_prefix_manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/appimage/WinePrefixManager.AppDir/wine_prefix_manager.png -------------------------------------------------------------------------------- /assets/icons/winehero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/assets/icons/winehero.jpg -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # IGDB (Twitch) API Credentials 2 | # Get these from: https://dev.twitch.tv/console/apps 3 | IGDB_CLIENT_ID=your_client_id_here 4 | IGDB_CLIENT_SECRET=your_client_secret_here 5 | 6 | # Optional: Override default API URLs (usually not needed) 7 | # TWITCH_OAUTH_URL=https://id.twitch.tv/oauth2/token 8 | # IGDB_API_BASE_URL=https://api.igdb.com/v4 9 | # IGDB_IMAGE_BASE_URL=https://images.igdb.com/igdb/image/upload -------------------------------------------------------------------------------- /lib/config/api_keys.dart: -------------------------------------------------------------------------------- 1 | // Store your API keys here. 2 | // IMPORTANT: Add this file to your .gitignore to avoid committing your secrets! 3 | 4 | import 'dart:io'; 5 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 6 | 7 | // IGDB API credentials - loaded from environment variables 8 | // These should be set in .env file for development or as environment variables in production 9 | String get globalIgdbClientId { 10 | // Try environment variable first (for production/CI), then .env file (for development) 11 | return Platform.environment['IGDB_CLIENT_ID'] ?? 12 | dotenv.env['IGDB_CLIENT_ID'] ?? 13 | ''; 14 | } 15 | 16 | String get globalIgdbClientSecret { 17 | // Try environment variable first (for production/CI), then .env file (for development) 18 | return Platform.environment['IGDB_CLIENT_SECRET'] ?? 19 | dotenv.env['IGDB_CLIENT_SECRET'] ?? 20 | ''; 21 | } 22 | 23 | // Optional URL overrides (usually not needed) 24 | String get globalTwitchOAuthUrl { 25 | return Platform.environment['TWITCH_OAUTH_URL'] ?? 26 | dotenv.env['TWITCH_OAUTH_URL'] ?? 27 | 'https://id.twitch.tv/oauth2/token'; 28 | } 29 | 30 | String get globalIgdbApiBaseUrl { 31 | return Platform.environment['IGDB_API_BASE_URL'] ?? 32 | dotenv.env['IGDB_API_BASE_URL'] ?? 33 | 'https://api.igdb.com/v4'; 34 | } 35 | 36 | String get globalIgdbImageBaseUrl { 37 | return Platform.environment['IGDB_IMAGE_BASE_URL'] ?? 38 | dotenv.env['IGDB_IMAGE_BASE_URL'] ?? 39 | 'https://images.igdb.com/igdb/image/upload'; 40 | } 41 | 42 | // If you also want to make these URLs fixed, uncomment and use them: 43 | // const String globalTwitchOAuthUrl = 'https://id.twitch.tv/oauth2/token'; 44 | // const String globalIgdbApiBaseUrl = 'https://api.igdb.com/v4'; 45 | // const String globalIgdbImageBaseUrl = 'https://images.igdb.com/igdb/image/upload'; -------------------------------------------------------------------------------- /lib/models/build_models.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; // Import dart:io for Platform 2 | import 'prefix_models.dart'; 3 | 4 | abstract class BaseBuild { 5 | final String name; 6 | final String? displayName; // Added for UI display 7 | final String? downloadUrl; // Made optional 8 | final String version; 9 | final PrefixType type; 10 | final String? installPath; // Added for locally installed builds 11 | final String? architecture; // Added architecture field (e.g., 'win32', 'win64', null if not applicable/known) 12 | 13 | BaseBuild({ 14 | required this.name, 15 | this.displayName, // Optional display name 16 | this.downloadUrl, // Optional 17 | required this.version, 18 | required this.type, 19 | this.installPath, // Optional 20 | this.architecture, // Optional 21 | }); 22 | 23 | // Getter to return display name or name if display name is null 24 | String get getDisplayName => displayName ?? name; 25 | } 26 | 27 | class WineBuild extends BaseBuild { 28 | WineBuild({ 29 | required super.name, 30 | super.displayName, 31 | required String downloadUrl, // Wine builds always have a download URL from API 32 | required super.version, 33 | super.architecture, // Pass to base 34 | }) : super(type: PrefixType.wine, downloadUrl: downloadUrl); // Pass URL to base 35 | 36 | factory WineBuild.fromGitHubAsset(Map asset, String version) { 37 | // Basic architecture detection from asset name (can be refined) 38 | String? detectedArch; 39 | String assetName = asset['name']?.toString().toLowerCase() ?? ''; 40 | if (assetName.contains('win64') || assetName.contains('x86_64') || assetName.contains('amd64')) { 41 | detectedArch = 'win64'; 42 | } else if (assetName.contains('win32') || assetName.contains('x86') && !assetName.contains('x86_64')) { 43 | detectedArch = 'win32'; 44 | } 45 | // If no specific arch detected in name, it might be a generic build or require deeper inspection later 46 | 47 | return WineBuild( 48 | name: asset['name'] ?? 'Unknown Wine Build', 49 | downloadUrl: asset['browser_download_url'] ?? '', 50 | version: version, 51 | architecture: detectedArch, 52 | ); 53 | } 54 | } 55 | 56 | class ProtonBuild extends BaseBuild { 57 | ProtonBuild({ 58 | required super.name, 59 | super.displayName, 60 | super.downloadUrl, // Can be null for installed Steam Proton 61 | required super.version, 62 | required PrefixType type, // Keep type for consistency, but it will be Proton for installed 63 | super.installPath, // Pass installPath to base 64 | // Proton is generally win64, so we can default it here or in the factory 65 | String? architecture = 'win64', 66 | }) : super(type: type, architecture: architecture); 67 | 68 | // Factory for downloadable Proton-GE builds 69 | factory ProtonBuild.fromGitHubRelease(Map release, PrefixType type) { 70 | final assets = release['assets'] as List; 71 | final tarballAsset = assets.firstWhere( 72 | (asset) => asset is Map && asset['name']?.toString().endsWith('.tar.gz') == true, 73 | orElse: () => throw Exception('No .tar.gz asset found in release ${release['tag_name']}'), 74 | ); 75 | 76 | final Map tarballAssetMap = Map.from(tarballAsset); 77 | 78 | return ProtonBuild( 79 | name: tarballAssetMap['name'] ?? 'Unknown Proton Build', 80 | downloadUrl: tarballAssetMap['browser_download_url'] ?? '', // Has download URL 81 | version: release['tag_name'] ?? 'unknown', 82 | type: type, // Use the passed type (should be Proton) 83 | architecture: 'win64', // Proton-GE is typically win64 84 | // installPath is null for downloaded builds 85 | ); 86 | } 87 | 88 | // Factory for locally installed Steam Proton builds 89 | // Updated: Removed type detection logic, always assumes Proton 90 | factory ProtonBuild.fromInstallPath(String path) { 91 | final dirName = path.split(Platform.pathSeparator).last; 92 | // Basic version extraction (might need refinement) 93 | // Keep the version extraction logic 94 | final version = dirName.replaceFirst('Proton ', '').replaceFirst('Experimental', '').trim(); 95 | // Removed type detection based on name 96 | // final type = dirName.contains('Experimental') 97 | // ? PrefixType.protonExperimental // Removed reference 98 | // : PrefixType.proton; 99 | 100 | return ProtonBuild( 101 | name: dirName, 102 | displayName: "$dirName (Installed)", // Indicate it's installed in display name 103 | version: version.isNotEmpty ? version : dirName, // Use dirName if version parsing fails 104 | type: PrefixType.proton, // Always assign Proton type for installed builds 105 | installPath: path, // Set the install path 106 | architecture: 'win64', // Installed Steam Proton is typically win64 107 | // downloadUrl is null 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/models/common_models.dart: -------------------------------------------------------------------------------- 1 | // This file should not define PrefixType and GameEntry that are already in prefix_models.dart 2 | // It can be kept as an empty file or with different utilities 3 | -------------------------------------------------------------------------------- /lib/models/igdb_models.dart: -------------------------------------------------------------------------------- 1 | class IgdbGame { 2 | final int id; 3 | final String name; 4 | final String? summary; 5 | final int? cover; 6 | final List screenshots; 7 | final List videos; // Add videos field 8 | 9 | IgdbGame({ 10 | required this.id, 11 | required this.name, 12 | this.summary, 13 | this.cover, 14 | this.screenshots = const [], 15 | this.videos = const [], // Initialize videos field 16 | }); 17 | 18 | factory IgdbGame.fromJson(Map json) { 19 | return IgdbGame( 20 | id: json['id'], 21 | name: json['name'], 22 | summary: json['summary'], 23 | cover: json['cover'], 24 | screenshots: json['screenshots'] != null 25 | ? List.from(json['screenshots']) 26 | : [], 27 | videos: json['videos'] != null 28 | ? List.from(json['videos']) 29 | : [], // Parse videos from JSON 30 | ); 31 | } 32 | } 33 | 34 | class IgdbCover { 35 | final int id; 36 | final String imageId; 37 | final int gameId; 38 | 39 | IgdbCover({ 40 | required this.id, 41 | required this.imageId, 42 | required this.gameId, 43 | }); 44 | 45 | factory IgdbCover.fromJson(Map json) { 46 | return IgdbCover( 47 | id: json['id'], 48 | imageId: json['image_id'], 49 | gameId: json['game'], 50 | ); 51 | } 52 | 53 | String get url => 'https://images.igdb.com/igdb/image/upload/t_cover_big/$imageId.jpg'; 54 | } 55 | 56 | class IgdbScreenshot { 57 | final int id; 58 | final String imageId; 59 | final int gameId; 60 | 61 | IgdbScreenshot({ 62 | required this.id, 63 | required this.imageId, 64 | required this.gameId, 65 | }); 66 | 67 | factory IgdbScreenshot.fromJson(Map json) { 68 | return IgdbScreenshot( 69 | id: json['id'], 70 | imageId: json['image_id'], 71 | gameId: json['game'], 72 | ); 73 | } 74 | 75 | String get url => 'https://images.igdb.com/igdb/image/upload/t_screenshot_big/$imageId.jpg'; 76 | } 77 | 78 | // Add a new class for game videos 79 | class IgdbGameVideo { 80 | final int id; 81 | final String videoId; 82 | final String name; 83 | final int gameId; 84 | 85 | IgdbGameVideo({ 86 | required this.id, 87 | required this.videoId, 88 | required this.name, 89 | required this.gameId, 90 | }); 91 | 92 | factory IgdbGameVideo.fromJson(Map json) { 93 | return IgdbGameVideo( 94 | id: json['id'], 95 | videoId: json['video_id'], 96 | name: json['name'], 97 | gameId: json['game'], 98 | ); 99 | } 100 | 101 | String get youtubeUrl => 'https://www.youtube.com/watch?v=$videoId'; 102 | String get thumbnailUrl => 'https://img.youtube.com/vi/$videoId/hqdefault.jpg'; 103 | } 104 | -------------------------------------------------------------------------------- /lib/models/wine_build.dart: -------------------------------------------------------------------------------- 1 | export 'prefix_models.dart'; 2 | export 'build_models.dart'; 3 | -------------------------------------------------------------------------------- /lib/pages/create_prefix_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import '../models/settings.dart'; 4 | import '../widgets/prefix_creation_form.dart'; 5 | import '../providers/settings_provider.dart'; 6 | import '../models/prefix_models.dart'; 7 | 8 | class CreatePrefixPage extends StatefulWidget { 9 | final VoidCallback? onSuccess; 10 | 11 | const CreatePrefixPage({Key? key, this.onSuccess}) : super(key: key); 12 | 13 | @override 14 | State createState() => _CreatePrefixPageState(); 15 | } 16 | 17 | class _CreatePrefixPageState extends State { 18 | PrefixType? _selectedType; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | // Get settings from provider 23 | final settingsProvider = Provider.of(context); 24 | final settings = settingsProvider.settings; 25 | 26 | // The main content is now returned directly, without Scaffold/AppBar 27 | return SingleChildScrollView( // Added SingleChildScrollView for dialog content 28 | child: Padding( 29 | padding: const EdgeInsets.all(24.0), // Increased padding for dialog look 30 | child: _selectedType == null 31 | // Step 1: Choose prefix type 32 | ? _buildPrefixTypeSelection() 33 | // Step 2: Show creation form based on selection 34 | : Column( 35 | mainAxisSize: MainAxisSize.min, // So column takes minimum space 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | children: [ 38 | // Header with back button 39 | Row( 40 | children: [ 41 | IconButton( 42 | icon: const Icon(Icons.arrow_back), 43 | onPressed: () { 44 | setState(() { 45 | _selectedType = null; 46 | }); 47 | }, 48 | ), 49 | Text( 50 | 'Create ${_selectedType == PrefixType.wine ? "Wine" : "Proton"} Prefix', // Updated title 51 | style: Theme.of(context).textTheme.headlineSmall, 52 | ), 53 | ], 54 | ), 55 | const SizedBox(height: 16), 56 | // Show the appropriate creation form 57 | // The PrefixCreationForm might need to be constrained in height 58 | // or ensure it's also scrollable if it becomes too large. 59 | Flexible( // Added Flexible to allow form to take available space 60 | child: PrefixCreationForm( 61 | settings: settings, 62 | initialPrefixType: _selectedType!, 63 | onSuccess: widget.onSuccess, 64 | // Potentially add an onCancel or onCreated callback 65 | // to close the dialog after creation/cancellation. 66 | ), 67 | ), 68 | ], 69 | ), 70 | ), 71 | ); 72 | } 73 | 74 | Widget _buildPrefixTypeSelection() { 75 | // This part remains largely the same, but consider dialog context 76 | return Column( // Changed from Center to Column for better dialog layout 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | mainAxisSize: MainAxisSize.min, 79 | children: [ 80 | Text( 81 | 'Select Prefix Type', 82 | style: Theme.of(context).textTheme.headlineMedium, 83 | ), 84 | const SizedBox(height: 32), 85 | Row( 86 | mainAxisAlignment: MainAxisAlignment.center, 87 | children: [ 88 | // Wine Option 89 | Expanded( 90 | child: _buildPrefixTypeCard( 91 | icon: Icons.wine_bar, 92 | title: 'Wine', 93 | description: 'Standard Wine prefix for running Windows applications', 94 | prefixType: PrefixType.wine, 95 | ), 96 | ), 97 | const SizedBox(width: 16), 98 | // Proton Option 99 | Expanded( 100 | child: _buildPrefixTypeCard( 101 | icon: Icons.games, 102 | title: 'Proton', 103 | description: 'Steam Proton prefix optimized for gaming', 104 | prefixType: PrefixType.proton, 105 | ), 106 | ), 107 | ], 108 | ), 109 | const SizedBox(height: 24), // Added some bottom padding 110 | TextButton( 111 | onPressed: () => Navigator.of(context).pop(), // Close button 112 | child: const Text('Cancel'), 113 | ), 114 | ], 115 | ); 116 | } 117 | 118 | Widget _buildPrefixTypeCard({ 119 | required IconData icon, 120 | required String title, 121 | required String description, 122 | required PrefixType prefixType, 123 | }) { 124 | return Card( 125 | elevation: 4, 126 | child: InkWell( 127 | onTap: () { 128 | setState(() { 129 | _selectedType = prefixType; 130 | }); 131 | }, 132 | child: Padding( 133 | padding: const EdgeInsets.all(24.0), 134 | child: Column( 135 | mainAxisSize: MainAxisSize.min, 136 | children: [ 137 | Icon(icon, size: 48), 138 | const SizedBox(height: 16), 139 | Text( 140 | title, 141 | style: Theme.of(context).textTheme.titleLarge, 142 | ), 143 | const SizedBox(height: 8), 144 | Text( 145 | description, 146 | textAlign: TextAlign.center, 147 | style: Theme.of(context).textTheme.bodyMedium, 148 | ), 149 | ], 150 | ), 151 | ), 152 | ), 153 | ); 154 | } 155 | } -------------------------------------------------------------------------------- /lib/pages/files_and_backup_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import '../models/prefix_models.dart'; 4 | import '../providers/prefix_provider.dart'; 5 | import 'file_manager_page.dart'; 6 | import 'backup_manager_page.dart'; 7 | 8 | class FilesAndBackupPage extends StatefulWidget { 9 | const FilesAndBackupPage({Key? key}) : super(key: key); 10 | 11 | @override 12 | State createState() => _FilesAndBackupPageState(); 13 | } 14 | 15 | class _FilesAndBackupPageState extends State with SingleTickerProviderStateMixin { 16 | late TabController _tabController; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | _tabController = TabController(length: 2, vsync: this); 22 | } 23 | 24 | @override 25 | void dispose() { 26 | _tabController.dispose(); 27 | super.dispose(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | final theme = Theme.of(context); 33 | 34 | return Scaffold( 35 | appBar: AppBar( 36 | title: const Text('Files & Backup'), 37 | automaticallyImplyLeading: false, 38 | elevation: 0, 39 | backgroundColor: theme.colorScheme.surface, 40 | bottom: TabBar( 41 | controller: _tabController, 42 | labelColor: theme.colorScheme.primary, 43 | unselectedLabelColor: theme.colorScheme.onSurface.withOpacity(0.6), 44 | indicatorColor: theme.colorScheme.primary, 45 | tabs: const [ 46 | Tab( 47 | icon: Icon(Icons.folder_copy_outlined), 48 | text: 'File Manager', 49 | ), 50 | Tab( 51 | icon: Icon(Icons.backup_outlined), 52 | text: 'Backup Manager', 53 | ), 54 | ], 55 | ), 56 | ), 57 | body: TabBarView( 58 | controller: _tabController, 59 | children: [ 60 | _buildFileManagerTab(), 61 | _buildBackupManagerTab(), 62 | ], 63 | ), 64 | ); 65 | } 66 | 67 | Widget _buildFileManagerTab() { 68 | return Consumer( 69 | builder: (context, prefixProvider, child) { 70 | final games = prefixProvider.getAllGamesFromPrefixes(); 71 | return FileManagerPage( 72 | game: games.isNotEmpty ? games.first : null, 73 | ); 74 | }, 75 | ); 76 | } 77 | 78 | Widget _buildBackupManagerTab() { 79 | return const BackupManagerPage(); 80 | } 81 | } -------------------------------------------------------------------------------- /lib/pages/iso_mounting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:file_picker/file_picker.dart'; 3 | import 'package:provider/provider.dart'; 4 | import '../models/prefix_models.dart'; 5 | import '../services/iso_mounting_service.dart'; 6 | import '../providers/prefix_provider.dart'; 7 | 8 | class IsoMountingPage extends StatefulWidget { 9 | const IsoMountingPage({super.key}); 10 | 11 | @override 12 | State createState() => _IsoMountingPageState(); 13 | } 14 | 15 | class _IsoMountingPageState extends State { 16 | String _statusMessage = ''; 17 | bool _isLoading = false; 18 | WinePrefix? _selectedPrefix; 19 | 20 | Future _selectAndMountIso() async { 21 | if (_selectedPrefix == null) { 22 | ScaffoldMessenger.of(context).showSnackBar( 23 | const SnackBar(content: Text('Please select a prefix first')), 24 | ); 25 | return; 26 | } 27 | 28 | final isoService = Provider.of(context, listen: false); 29 | 30 | try { 31 | // Pick ISO file 32 | final result = await FilePicker.platform.pickFiles( 33 | type: FileType.custom, 34 | allowedExtensions: ['iso', 'img', 'bin', 'cue'], 35 | dialogTitle: 'Select ISO file to mount', 36 | ); 37 | 38 | if (result == null || result.files.isEmpty) { 39 | return; // User cancelled 40 | } 41 | 42 | final isoPath = result.files.first.path!; 43 | 44 | if (mounted) { 45 | setState(() { 46 | _isLoading = true; 47 | _statusMessage = 'Mounting ISO...'; 48 | }); 49 | } 50 | 51 | final success = await isoService.mountIso( 52 | prefix: _selectedPrefix!, 53 | isoPath: isoPath, 54 | onStatusUpdate: (message) { 55 | if (mounted) { 56 | setState(() { 57 | _statusMessage = message; 58 | }); 59 | } 60 | }, 61 | ); 62 | 63 | if (mounted) { 64 | if (success) { 65 | ScaffoldMessenger.of(context).showSnackBar( 66 | const SnackBar(content: Text('ISO mounted successfully as CD drive D:')), 67 | ); 68 | } else { 69 | ScaffoldMessenger.of(context).showSnackBar( 70 | const SnackBar( 71 | content: Text('Failed to mount ISO'), 72 | backgroundColor: Colors.red, 73 | ), 74 | ); 75 | } 76 | } 77 | } catch (e) { 78 | if (mounted) { 79 | ScaffoldMessenger.of(context).showSnackBar( 80 | SnackBar( 81 | content: Text('Error mounting ISO: $e'), 82 | backgroundColor: Colors.red, 83 | ), 84 | ); 85 | } 86 | } finally { 87 | if (mounted) { 88 | setState(() { 89 | _isLoading = false; 90 | _statusMessage = ''; 91 | }); 92 | } 93 | } 94 | } 95 | 96 | Future _unmountIso(WinePrefix prefix) async { 97 | final isoService = Provider.of(context, listen: false); 98 | 99 | try { 100 | if (mounted) { 101 | setState(() { 102 | _isLoading = true; 103 | _statusMessage = 'Unmounting ISO...'; 104 | }); 105 | } 106 | 107 | final success = await isoService.unmountIso( 108 | prefix: prefix, 109 | onStatusUpdate: (message) { 110 | if (mounted) { 111 | setState(() { 112 | _statusMessage = message; 113 | }); 114 | } 115 | }, 116 | ); 117 | 118 | if (mounted) { 119 | if (success) { 120 | ScaffoldMessenger.of(context).showSnackBar( 121 | const SnackBar(content: Text('ISO ejected successfully')), 122 | ); 123 | } else { 124 | ScaffoldMessenger.of(context).showSnackBar( 125 | const SnackBar( 126 | content: Text('Failed to eject ISO'), 127 | backgroundColor: Colors.red, 128 | ), 129 | ); 130 | } 131 | } 132 | } catch (e) { 133 | if (mounted) { 134 | ScaffoldMessenger.of(context).showSnackBar( 135 | SnackBar( 136 | content: Text('Error ejecting ISO: $e'), 137 | backgroundColor: Colors.red, 138 | ), 139 | ); 140 | } 141 | } finally { 142 | if (mounted) { 143 | setState(() { 144 | _isLoading = false; 145 | _statusMessage = ''; 146 | }); 147 | } 148 | } 149 | } 150 | 151 | @override 152 | Widget build(BuildContext context) { 153 | final prefixProvider = Provider.of(context); 154 | final isoService = Provider.of(context); 155 | final prefixes = prefixProvider.prefixes; 156 | final mountedIsos = isoService.getAllMountedIsos(); 157 | 158 | return Scaffold( 159 | appBar: AppBar( 160 | title: const Text('ISO/CD Management'), 161 | ), 162 | body: SingleChildScrollView( 163 | padding: const EdgeInsets.all(16.0), 164 | child: Column( 165 | crossAxisAlignment: CrossAxisAlignment.stretch, 166 | children: [ 167 | // Title 168 | const Text( 169 | 'Mount ISO Files', 170 | style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), 171 | ), 172 | const SizedBox(height: 8), 173 | const Text( 174 | 'Mount ISO files as virtual CD drives in Wine prefixes', 175 | style: TextStyle(fontSize: 16), 176 | ), 177 | const SizedBox(height: 24), 178 | 179 | // No prefixes message 180 | if (prefixes.isEmpty) ...[ 181 | const Card( 182 | child: Padding( 183 | padding: EdgeInsets.all(24.0), 184 | child: Column( 185 | children: [ 186 | Icon(Icons.folder_off, size: 48), 187 | SizedBox(height: 16), 188 | Text( 189 | 'No Wine prefixes found', 190 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 191 | ), 192 | SizedBox(height: 8), 193 | Text('Create a prefix first to mount ISOs'), 194 | ], 195 | ), 196 | ), 197 | ), 198 | ], 199 | 200 | // Mount controls 201 | if (prefixes.isNotEmpty) ...[ 202 | Card( 203 | child: Padding( 204 | padding: const EdgeInsets.all(16.0), 205 | child: Column( 206 | crossAxisAlignment: CrossAxisAlignment.stretch, 207 | children: [ 208 | // Prefix dropdown 209 | DropdownButtonFormField( 210 | value: _selectedPrefix, 211 | decoration: const InputDecoration( 212 | labelText: 'Select Wine Prefix', 213 | border: OutlineInputBorder(), 214 | ), 215 | items: prefixes.map((prefix) { 216 | return DropdownMenuItem( 217 | value: prefix, 218 | child: Text(prefix.name), 219 | ); 220 | }).toList(), 221 | onChanged: _isLoading ? null : (prefix) { 222 | setState(() { 223 | _selectedPrefix = prefix; 224 | }); 225 | }, 226 | ), 227 | const SizedBox(height: 16), 228 | 229 | // Mount button 230 | ElevatedButton.icon( 231 | onPressed: (_isLoading || _selectedPrefix == null) 232 | ? null 233 | : _selectAndMountIso, 234 | icon: _isLoading 235 | ? const SizedBox( 236 | width: 16, 237 | height: 16, 238 | child: CircularProgressIndicator(strokeWidth: 2), 239 | ) 240 | : const Icon(Icons.add_circle_outline), 241 | label: const Text('Mount ISO'), 242 | ), 243 | 244 | // Status message 245 | if (_statusMessage.isNotEmpty) ...[ 246 | const SizedBox(height: 16), 247 | Container( 248 | padding: const EdgeInsets.all(12), 249 | decoration: BoxDecoration( 250 | color: Colors.blue.shade50, 251 | borderRadius: BorderRadius.circular(8), 252 | border: Border.all(color: Colors.blue.shade200), 253 | ), 254 | child: Text( 255 | _statusMessage, 256 | style: const TextStyle(color: Colors.blue), 257 | ), 258 | ), 259 | ], 260 | ], 261 | ), 262 | ), 263 | ), 264 | const SizedBox(height: 24), 265 | ], 266 | 267 | // Mounted ISOs section 268 | const Text( 269 | 'Currently Mounted ISOs', 270 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 271 | ), 272 | const SizedBox(height: 12), 273 | 274 | // Mounted ISOs list 275 | if (mountedIsos.isEmpty) 276 | const Card( 277 | child: Padding( 278 | padding: EdgeInsets.all(32.0), 279 | child: Column( 280 | children: [ 281 | Icon(Icons.disc_full, size: 48, color: Colors.grey), 282 | SizedBox(height: 16), 283 | Text( 284 | 'No ISOs currently mounted', 285 | style: TextStyle(fontSize: 16, color: Colors.grey), 286 | ), 287 | SizedBox(height: 8), 288 | Text( 289 | 'Mount an ISO to see it here', 290 | style: TextStyle(color: Colors.grey), 291 | ), 292 | ], 293 | ), 294 | ), 295 | ) 296 | else 297 | ...mountedIsos.map((mountedIso) => Card( 298 | margin: const EdgeInsets.only(bottom: 8), 299 | child: ListTile( 300 | leading: const Icon(Icons.album), 301 | title: Text(mountedIso.prefix.name), 302 | subtitle: Text('ISO: ${mountedIso.isoPath.split('/').last}'), 303 | trailing: IconButton( 304 | onPressed: _isLoading 305 | ? null 306 | : () => _unmountIso(mountedIso.prefix), 307 | icon: const Icon(Icons.eject), 308 | tooltip: 'Eject ISO', 309 | ), 310 | ), 311 | )).toList(), 312 | ], 313 | ), 314 | ), 315 | ); 316 | } 317 | } -------------------------------------------------------------------------------- /lib/providers/settings_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/settings.dart'; 3 | 4 | /// Provider class for application settings 5 | /// This allows for reactive updates when settings change 6 | class SettingsProvider extends ChangeNotifier { 7 | Settings _settings; 8 | 9 | SettingsProvider(this._settings); 10 | 11 | Settings get settings => _settings; 12 | 13 | /// Updates settings and notifies listeners 14 | Future updateSettings(Settings settings) async { 15 | _settings = settings; 16 | await AppSettings.save(_settings); 17 | notifyListeners(); 18 | } 19 | 20 | /// Updates settings and notifies listeners 21 | Future updateIgdbToken(String token, Duration expiry) async { 22 | final updatedSettings = await AppSettings.updateToken(_settings, token, expiry); 23 | _settings = updatedSettings; 24 | notifyListeners(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/providers/window_control_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:window_manager/window_manager.dart'; 3 | 4 | /// Provider class for window control buttons to centralize window management 5 | /// and prevent duplicate controls across different pages 6 | class WindowControlProvider { 7 | /// Get window control buttons as a list of widgets 8 | List getWindowButtons() { 9 | return [ 10 | IconButton( 11 | icon: const Icon(Icons.minimize), 12 | onPressed: () => windowManager.minimize(), 13 | tooltip: 'Minimize', 14 | ), 15 | IconButton( 16 | icon: const Icon(Icons.crop_square), 17 | onPressed: () async { 18 | if (await windowManager.isMaximized()) { 19 | windowManager.unmaximize(); 20 | } else { 21 | windowManager.maximize(); 22 | } 23 | }, 24 | tooltip: 'Maximize', 25 | ), 26 | IconButton( 27 | icon: const Icon(Icons.close), 28 | onPressed: () => windowManager.close(), 29 | tooltip: 'Close', 30 | ), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/services/backup_service.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/lib/services/backup_service.dart -------------------------------------------------------------------------------- /lib/services/compressed_game_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | import 'package:path/path.dart' as p; 4 | import '../models/prefix_models.dart'; 5 | import 'log_service.dart'; 6 | 7 | class CompressedGameService { 8 | final LogService _logService = LogService(); 9 | final Map _activeSessions = {}; 10 | 11 | /// Extracts a compressed game before launching 12 | Future extractGameForLaunch(ExeEntry compressedGame) async { 13 | if (!compressedGame.isCompressed || compressedGame.compressedArchivePath == null) { 14 | throw Exception('Game is not a compressed game'); 15 | } 16 | 17 | final archivePath = compressedGame.compressedArchivePath!; 18 | final extractPath = compressedGame.extractedBasePath!; 19 | 20 | _logService.log('Extracting compressed game: ${compressedGame.name}'); 21 | 22 | // Create extract directory if it doesn't exist 23 | final extractDir = Directory(extractPath); 24 | if (!await extractDir.exists()) { 25 | await extractDir.create(recursive: true); 26 | } 27 | 28 | // Check if already extracted and up-to-date 29 | final extractedGamePath = await _findGameExecutable(extractPath); 30 | if (extractedGamePath != null && await File(extractedGamePath).exists()) { 31 | final archiveModified = await File(archivePath).lastModified(); 32 | final extractedModified = await File(extractedGamePath).lastModified(); 33 | 34 | if (extractedModified.isAfter(archiveModified)) { 35 | _logService.log('Game already extracted and up-to-date'); 36 | return extractedGamePath; 37 | } 38 | } 39 | 40 | // Create extraction script 41 | final tempDir = Directory.systemTemp.createTempSync('game_extract_'); 42 | final scriptPath = p.join(tempDir.path, 'extract_script.sh'); 43 | 44 | String extractCommand; 45 | final fileName = p.basename(archivePath).toLowerCase(); 46 | 47 | if (fileName.endsWith('.tar.zst')) { 48 | extractCommand = 'zstd -d "$archivePath" -c | tar xf - -C "$extractPath"'; 49 | } else if (fileName.endsWith('.tar.gz') || fileName.endsWith('.tgz')) { 50 | extractCommand = 'tar xzf "$archivePath" -C "$extractPath"'; 51 | } else if (fileName.endsWith('.tar.xz')) { 52 | extractCommand = 'tar xJf "$archivePath" -C "$extractPath"'; 53 | } else if (fileName.endsWith('.zip')) { 54 | extractCommand = 'unzip -q -o "$archivePath" -d "$extractPath"'; 55 | } else if (fileName.endsWith('.7z')) { 56 | extractCommand = '7z x "$archivePath" -o"$extractPath" -y'; 57 | } else { 58 | throw Exception('Unsupported archive format: $fileName'); 59 | } 60 | 61 | final scriptContent = '''#!/bin/bash 62 | set -e 63 | echo "Extracting ${compressedGame.name}..." 64 | $extractCommand 65 | echo "Extraction complete!" 66 | '''; 67 | 68 | await File(scriptPath).writeAsString(scriptContent); 69 | await Process.run('chmod', ['+x', scriptPath]); 70 | 71 | // Run extraction 72 | final result = await Process.run('bash', [scriptPath]); 73 | 74 | // Cleanup temp script 75 | await tempDir.delete(recursive: true); 76 | 77 | if (result.exitCode != 0) { 78 | throw Exception('Extraction failed: ${result.stderr}'); 79 | } 80 | 81 | // Find the game executable 82 | final gameExePath = await _findGameExecutable(extractPath); 83 | if (gameExePath == null) { 84 | throw Exception('Could not find game executable after extraction'); 85 | } 86 | 87 | // Create session to monitor file changes 88 | final session = CompressedGameSession( 89 | originalGame: compressedGame, 90 | extractedPath: extractPath, 91 | gameExecutable: gameExePath, 92 | startTime: DateTime.now(), 93 | ); 94 | 95 | _activeSessions[compressedGame.path] = session; 96 | 97 | _logService.log('Game extracted successfully: $gameExePath'); 98 | return gameExePath; 99 | } 100 | 101 | /// Monitors for file changes and offers recompression after game ends 102 | Future handleGameExit(ExeEntry compressedGame, WinePrefix prefix) async { 103 | final session = _activeSessions[compressedGame.path]; 104 | if (session == null) return; 105 | 106 | _logService.log('Checking for file changes in ${compressedGame.name}'); 107 | 108 | // Check if files have been modified since extraction 109 | final hasChanges = await _detectFileChanges(session); 110 | 111 | if (hasChanges) { 112 | _logService.log('File changes detected, offering recompression'); 113 | // This would trigger UI notification for recompression 114 | // For now, just set the flag 115 | await _markGameNeedsRecompression(compressedGame, prefix); 116 | } 117 | 118 | // Remove session 119 | _activeSessions.remove(compressedGame.path); 120 | } 121 | 122 | /// Creates a new compressed archive with updated files 123 | Future recompressGame(ExeEntry compressedGame, List additionalPaths) async { 124 | if (!compressedGame.isCompressed || compressedGame.compressedArchivePath == null) { 125 | throw Exception('Game is not a compressed game'); 126 | } 127 | 128 | final archivePath = compressedGame.compressedArchivePath!; 129 | final extractPath = compressedGame.extractedBasePath!; 130 | 131 | _logService.log('Recompressing game: ${compressedGame.name}'); 132 | 133 | // Create backup of original archive 134 | final backupPath = '$archivePath.backup.${DateTime.now().millisecondsSinceEpoch}'; 135 | await File(archivePath).copy(backupPath); 136 | 137 | try { 138 | // Create new archive with updated files 139 | final tempDir = Directory.systemTemp.createTempSync('recompress_'); 140 | final scriptPath = p.join(tempDir.path, 'recompress_script.sh'); 141 | 142 | String compressCommand; 143 | final fileName = p.basename(archivePath).toLowerCase(); 144 | 145 | // Build list of paths to include 146 | final pathsToInclude = [extractPath, ...additionalPaths]; 147 | 148 | if (fileName.endsWith('.tar.zst')) { 149 | compressCommand = ''' 150 | cd "${p.dirname(extractPath)}" 151 | tar cf - "${p.basename(extractPath)}" ${additionalPaths.map((path) => '"${p.basename(path)}"').join(' ')} | zstd -3 -o "$archivePath" 152 | '''; 153 | } else if (fileName.endsWith('.tar.gz')) { 154 | compressCommand = ''' 155 | cd "${p.dirname(extractPath)}" 156 | tar czf "$archivePath" "${p.basename(extractPath)}" ${additionalPaths.map((path) => '"${p.basename(path)}"').join(' ')} 157 | '''; 158 | } else { 159 | throw Exception('Recompression not supported for this format yet'); 160 | } 161 | 162 | final scriptContent = '''#!/bin/bash 163 | set -e 164 | echo "Recompressing ${compressedGame.name}..." 165 | $compressCommand 166 | echo "Recompression complete!" 167 | '''; 168 | 169 | await File(scriptPath).writeAsString(scriptContent); 170 | await Process.run('chmod', ['+x', scriptPath]); 171 | 172 | // Run recompression 173 | final result = await Process.run('bash', [scriptPath]); 174 | 175 | // Cleanup temp script 176 | await tempDir.delete(recursive: true); 177 | 178 | if (result.exitCode != 0) { 179 | throw Exception('Recompression failed: ${result.stderr}'); 180 | } 181 | 182 | // Remove backup on success 183 | await File(backupPath).delete(); 184 | 185 | _logService.log('Game recompressed successfully'); 186 | 187 | } catch (e) { 188 | // Restore backup on failure 189 | await File(backupPath).copy(archivePath); 190 | await File(backupPath).delete(); 191 | rethrow; 192 | } 193 | } 194 | 195 | /// Finds the game executable in the extracted directory 196 | Future _findGameExecutable(String extractPath) async { 197 | final dir = Directory(extractPath); 198 | if (!await dir.exists()) return null; 199 | 200 | // Look for exe files 201 | await for (final entity in dir.list(recursive: true)) { 202 | if (entity is File && entity.path.toLowerCase().endsWith('.exe')) { 203 | final name = p.basename(entity.path).toLowerCase(); 204 | // Skip common system/installer files 205 | if (!name.contains('unins') && 206 | !name.contains('setup') && 207 | !name.contains('install') && 208 | !name.contains('redist') && 209 | !name.contains('vcredist') && 210 | !name.contains('directx')) { 211 | return entity.path; 212 | } 213 | } 214 | } 215 | return null; 216 | } 217 | 218 | /// Detects if files have been modified since extraction 219 | Future _detectFileChanges(CompressedGameSession session) async { 220 | try { 221 | final dir = Directory(session.extractedPath); 222 | if (!await dir.exists()) return false; 223 | 224 | await for (final entity in dir.list(recursive: true)) { 225 | if (entity is File) { 226 | final lastModified = await entity.lastModified(); 227 | if (lastModified.isAfter(session.startTime)) { 228 | return true; 229 | } 230 | } 231 | } 232 | return false; 233 | } catch (e) { 234 | _logService.log('Error detecting file changes: $e', LogLevel.warning); 235 | return false; 236 | } 237 | } 238 | 239 | /// Marks a game as needing recompression 240 | Future _markGameNeedsRecompression(ExeEntry compressedGame, WinePrefix prefix) async { 241 | // This would update the game entry in the prefix 242 | // Implementation depends on how we want to handle this in the UI 243 | _logService.log('Game ${compressedGame.name} marked as needing recompression'); 244 | } 245 | 246 | /// Cleans up extracted files for a compressed game 247 | Future cleanupExtractedGame(ExeEntry compressedGame) async { 248 | if (!compressedGame.isCompressed || compressedGame.extractedBasePath == null) { 249 | return; 250 | } 251 | 252 | final extractPath = compressedGame.extractedBasePath!; 253 | final extractDir = Directory(extractPath); 254 | 255 | if (await extractDir.exists()) { 256 | await extractDir.delete(recursive: true); 257 | _logService.log('Cleaned up extracted files for ${compressedGame.name}'); 258 | } 259 | } 260 | } 261 | 262 | class CompressedGameSession { 263 | final ExeEntry originalGame; 264 | final String extractedPath; 265 | final String gameExecutable; 266 | final DateTime startTime; 267 | 268 | CompressedGameSession({ 269 | required this.originalGame, 270 | required this.extractedPath, 271 | required this.gameExecutable, 272 | required this.startTime, 273 | }); 274 | } -------------------------------------------------------------------------------- /lib/services/cover_art_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | // import 'dart:typed_data'; // Unused import removed 3 | // import 'package:flutter/foundation.dart'; // Unused import removed 4 | import 'package:http/http.dart' as http; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:path/path.dart' as p; 7 | 8 | class CoverArtService { 9 | static const String _imageCacheDirName = 'image_cache'; 10 | 11 | Future _getImageCacheDirectory() async { 12 | final appSupportDir = await getApplicationSupportDirectory(); 13 | final imageDir = Directory(p.join(appSupportDir.path, _imageCacheDirName)); 14 | if (!await imageDir.exists()) { 15 | await imageDir.create(recursive: true); 16 | } 17 | return imageDir; 18 | } 19 | 20 | Future getImageCacheDirectoryPath() async { 21 | final dir = await _getImageCacheDirectory(); 22 | return dir.path; 23 | } 24 | 25 | String _generateFilename(int igdbId, String coverUrl) { 26 | final extension = p.extension(coverUrl).split('?').first; 27 | return '$igdbId${extension.isNotEmpty ? extension : '.jpg'}'; 28 | } 29 | 30 | Future downloadAndSaveCover(int igdbId, String coverUrl) async { 31 | if (coverUrl.isEmpty) return null; 32 | try { 33 | final imageDir = await _getImageCacheDirectory(); 34 | final filename = _generateFilename(igdbId, coverUrl); 35 | final filePath = p.join(imageDir.path, filename); 36 | final file = File(filePath); 37 | 38 | // debugPrint('Attempting to download cover from: $coverUrl'); 39 | final response = await http.get(Uri.parse(coverUrl)); 40 | 41 | if (response.statusCode == 200) { 42 | await file.writeAsBytes(response.bodyBytes); 43 | // debugPrint('Saved cover for $igdbId to $filePath'); 44 | return filePath; 45 | } else { 46 | // debugPrint('Failed to download cover for $igdbId: ${response.statusCode}'); 47 | return null; 48 | } 49 | } catch (e) { 50 | // debugPrint('Error downloading/saving cover for $igdbId: $e'); 51 | return null; 52 | } 53 | } 54 | 55 | Future getLocalCoverPath(int igdbId, String? coverUrl) async { 56 | if (coverUrl == null || coverUrl.isEmpty) return null; 57 | try { 58 | final imageDir = await _getImageCacheDirectory(); 59 | final filename = _generateFilename(igdbId, coverUrl); 60 | final filePath = p.join(imageDir.path, filename); 61 | final file = File(filePath); 62 | 63 | if (await file.exists()) { 64 | return filePath; 65 | } else { 66 | // Attempt download only if file doesn't exist 67 | return await downloadAndSaveCover(igdbId, coverUrl); 68 | } 69 | } catch (e) { 70 | // debugPrint('Error getting local cover path for $igdbId: $e'); 71 | return null; 72 | } 73 | } 74 | 75 | // --- Screenshot Handling --- 76 | 77 | String _generateScreenshotFilename(String screenshotUrl) { 78 | final urlHash = screenshotUrl.hashCode.toRadixString(16); 79 | final extension = p.extension(screenshotUrl).split('?').first; 80 | return 'ss_${urlHash}${extension.isNotEmpty ? extension : '.jpg'}'; 81 | } 82 | 83 | Future _downloadAndSaveScreenshot(String screenshotUrl) async { 84 | if (screenshotUrl.isEmpty) return null; 85 | try { 86 | final imageDir = await _getImageCacheDirectory(); 87 | final filename = _generateScreenshotFilename(screenshotUrl); 88 | final filePath = p.join(imageDir.path, filename); 89 | final file = File(filePath); 90 | 91 | // debugPrint('Attempting to download screenshot from: $screenshotUrl'); 92 | final response = await http.get(Uri.parse(screenshotUrl)); 93 | 94 | if (response.statusCode == 200) { 95 | await file.writeAsBytes(response.bodyBytes); 96 | // debugPrint('Saved screenshot to $filePath'); 97 | return filePath; 98 | } else { 99 | // debugPrint('Failed to download screenshot $screenshotUrl: ${response.statusCode}'); 100 | return null; 101 | } 102 | } catch (e) { 103 | // debugPrint('Error downloading/saving screenshot $screenshotUrl: $e'); 104 | return null; 105 | } 106 | } 107 | 108 | Future getLocalScreenshotPath(String? screenshotUrl) async { 109 | if (screenshotUrl == null || screenshotUrl.isEmpty) return null; 110 | try { 111 | final imageDir = await _getImageCacheDirectory(); 112 | final filename = _generateScreenshotFilename(screenshotUrl); 113 | final filePath = p.join(imageDir.path, filename); 114 | final file = File(filePath); 115 | 116 | if (await file.exists()) { 117 | return filePath; 118 | } else { 119 | // Attempt download only if file doesn't exist 120 | return await _downloadAndSaveScreenshot(screenshotUrl); 121 | } 122 | } catch (e) { 123 | // debugPrint('Error getting local screenshot path for $screenshotUrl: $e'); 124 | return null; 125 | } 126 | } 127 | 128 | Future> getLocalScreenshotPaths(List screenshotUrls) async { 129 | final List localPaths = []; 130 | for (final url in screenshotUrls) { 131 | final localPath = await getLocalScreenshotPath(url); 132 | if (localPath != null) { 133 | localPaths.add(localPath); 134 | } 135 | } 136 | return localPaths; 137 | } 138 | 139 | // --- End Screenshot Handling --- 140 | 141 | Future deleteCover(String? localCoverPath) async { 142 | if (localCoverPath == null || localCoverPath.isEmpty) return; 143 | try { 144 | final file = File(localCoverPath); 145 | if (await file.exists()) { 146 | await file.delete(); 147 | // debugPrint('Deleted cover: $localCoverPath'); 148 | } 149 | } catch (e) { 150 | // debugPrint('Error deleting cover $localCoverPath: $e'); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /lib/services/igdb_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | // import 'package:flutter/foundation.dart'; // Unused import removed 3 | import 'package:http/http.dart' as http; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:provider/provider.dart'; 6 | import '../models/settings.dart'; 7 | import '../models/igdb_models.dart'; 8 | import '../providers/settings_provider.dart'; 9 | import '../config/api_keys.dart'; // Import the new api_keys file 10 | 11 | class IgdbService { 12 | 13 | Future?> getIgdbToken(Settings settings) async { 14 | // Use global constants for Client ID and Secret 15 | if (globalIgdbClientId.isEmpty || globalIgdbClientSecret.isEmpty) { 16 | // debugPrint('[IgdbService] Global IGDB credentials not set.'); 17 | return null; 18 | } 19 | 20 | if (settings.igdbAccessToken != null && 21 | settings.igdbTokenExpiry != null && 22 | settings.igdbTokenExpiry!.isAfter(DateTime.now())) { 23 | // debugPrint('[IgdbService] Using existing IGDB token.'); 24 | return { 25 | 'token': settings.igdbAccessToken!, 26 | 'expiry': settings.igdbTokenExpiry!, 27 | 'isNew': false, 28 | }; 29 | } 30 | 31 | // debugPrint('[IgdbService] Fetching new IGDB token...'); 32 | try { 33 | final response = await http.post( 34 | Uri.parse(settings.twitchOAuthUrl), // Use settings URL 35 | body: { 36 | 'client_id': globalIgdbClientId, // Use global constant 37 | 'client_secret': globalIgdbClientSecret, // Use global constant 38 | 'grant_type': 'client_credentials', 39 | }, 40 | ); 41 | 42 | if (response.statusCode == 200) { 43 | final data = json.decode(response.body); 44 | final token = data['access_token'] as String; 45 | final expiresIn = Duration(seconds: data['expires_in'] as int); 46 | final expiryTime = DateTime.now().add(expiresIn); 47 | // debugPrint('[IgdbService] Successfully fetched new IGDB token.'); 48 | return { 49 | 'token': token, 50 | 'expiry': expiryTime, 51 | 'isNew': true, 52 | }; 53 | } else { 54 | // debugPrint('[IgdbService] Failed to get IGDB token: ${response.statusCode} ${response.body}'); 55 | } 56 | } catch (e) { 57 | // debugPrint('[IgdbService] Error getting IGDB token: $e'); 58 | } 59 | return null; 60 | } 61 | 62 | Future> searchIgdbGames(String query, Settings settings, String token) async { 63 | // debugPrint('[IgdbService] Searching IGDB for: "$query"'); 64 | final String requestBody = 'search "$query"; fields name,cover,screenshots,videos,summary; where platforms = (6); limit 20;'; // PC platform = 6 65 | // debugPrint('[IgdbService] Request Body: $requestBody'); 66 | try { 67 | final response = await http.post( 68 | Uri.parse(settings.igdbApiBaseUrl).replace(path: '/v4/games'), // Use replace 69 | headers: { 70 | 'Accept': 'application/json', 71 | 'Client-ID': globalIgdbClientId, // Use global constant 72 | 'Authorization': 'Bearer $token', 73 | }, 74 | body: requestBody, 75 | ); 76 | 77 | // debugPrint('[IgdbService] Search Response Status: ${response.statusCode}'); 78 | // Added logging for raw response body 79 | // debugPrint('[IgdbService] Search Response Body Raw: ${response.body}'); 80 | 81 | if (response.statusCode == 200) { 82 | final List gamesJson = json.decode(response.body); 83 | // debugPrint('[IgdbService] Parsed ${gamesJson.length} games from JSON.'); 84 | // Add detailed logging for each parsed game 85 | final gamesList = gamesJson.map((g) { 86 | // debugPrint('[IgdbService] Parsing game JSON: $g'); 87 | final igdbGame = IgdbGame.fromJson(g); 88 | // debugPrint('[IgdbService] Parsed IgdbGame: id=${igdbGame.id}, name=${igdbGame.name}, cover=${igdbGame.cover}, screenshots=${igdbGame.screenshots}'); 89 | return igdbGame; 90 | }).toList(); 91 | return gamesList; 92 | } else { 93 | // debugPrint('[IgdbService] IGDB API error during search: ${response.statusCode} ${response.body}'); 94 | } 95 | } catch (e) { 96 | // debugPrint('[IgdbService] Error searching IGDB: $e'); 97 | } 98 | return []; 99 | } 100 | 101 | Future?> fetchCoverDetails(int? coverId, Settings settings, String token) async { 102 | if (coverId == null) return null; 103 | // debugPrint('[IgdbService] Fetching cover details for ID: $coverId'); 104 | try { 105 | final response = await http.post( 106 | Uri.parse(settings.igdbApiBaseUrl).replace(path: '/v4/covers'), // Use replace 107 | headers: { 108 | 'Accept': 'application/json', 109 | 'Client-ID': globalIgdbClientId, // Use global constant 110 | 'Authorization': 'Bearer $token', 111 | }, 112 | body: 'fields image_id, url; where id = $coverId;', // Request URL field too 113 | ); 114 | 115 | if (response.statusCode == 200) { 116 | final List covers = json.decode(response.body); 117 | if (covers.isNotEmpty) { 118 | final coverData = covers[0]; 119 | final imageId = coverData['image_id']?.toString(); 120 | String? imageUrl = coverData['url']?.toString(); 121 | // debugPrint('[IgdbService] Cover API Response Data: $coverData'); 122 | 123 | if (imageUrl != null && imageUrl.isNotEmpty) { 124 | if (imageUrl.startsWith('//')) { 125 | imageUrl = 'https:$imageUrl'; 126 | } 127 | imageUrl = imageUrl.replaceFirst('/t_thumb/', '/t_cover_big/'); 128 | // debugPrint("[IgdbService] Using direct URL from API for cover: $imageUrl"); 129 | return {'url': imageUrl, 'imageId': imageId ?? ''}; 130 | } else if (imageId != null) { 131 | // debugPrint("[IgdbService] Constructing cover URL from imageId as 'url' field was missing."); 132 | imageUrl = '${settings.igdbImageBaseUrl}/t_cover_big/$imageId.jpg'; 133 | return {'url': imageUrl, 'imageId': imageId}; 134 | } else { 135 | // debugPrint("[IgdbService] Cover details fetched but no URL or imageId found."); 136 | } 137 | } else { 138 | // debugPrint("[IgdbService] Cover details fetched but response list was empty for ID: $coverId"); 139 | } 140 | } else { 141 | // debugPrint('[IgdbService] IGDB API error fetching cover: ${response.statusCode} ${response.body}'); 142 | } 143 | } catch (e) { 144 | // debugPrint('[IgdbService] Error fetching cover: $e'); 145 | } 146 | return null; 147 | } 148 | 149 | Future>> fetchScreenshotDetails(List screenshotIds, Settings settings, String token) async { 150 | if (screenshotIds.isEmpty) return []; 151 | // debugPrint('[IgdbService] Fetching screenshot details for IDs: $screenshotIds'); 152 | try { 153 | final requestBody = 'fields image_id, url; where id = (${screenshotIds.join(",")}); limit ${screenshotIds.length};'; 154 | final response = await http.post( 155 | Uri.parse(settings.igdbApiBaseUrl).replace(path: '/v4/screenshots'), // Use replace 156 | headers: { 157 | 'Accept': 'application/json', 158 | 'Client-ID': globalIgdbClientId, // Use global constant 159 | 'Authorization': 'Bearer $token', 160 | }, 161 | body: requestBody, 162 | ); 163 | 164 | if (response.statusCode == 200) { 165 | final List screenshots = json.decode(response.body); 166 | // debugPrint('[IgdbService] Screenshot API Response Data: $screenshots'); 167 | List> results = []; 168 | for (var s in screenshots) { 169 | final imageId = s['image_id']?.toString(); 170 | String? imageUrl = s['url']?.toString(); 171 | 172 | if (imageUrl != null && imageUrl.isNotEmpty) { 173 | if (imageUrl.startsWith('//')) { 174 | imageUrl = 'https:$imageUrl'; 175 | } 176 | imageUrl = imageUrl.replaceFirst('/t_thumb/', '/t_screenshot_big/'); 177 | // debugPrint("[IgdbService] Using direct URL from API for screenshot: $imageUrl"); 178 | results.add({'url': imageUrl, 'imageId': imageId ?? ''}); 179 | } else if (imageId != null) { 180 | // debugPrint("[IgdbService] Constructing screenshot URL from imageId as 'url' field was missing."); 181 | imageUrl = '${settings.igdbImageBaseUrl}/t_screenshot_big/$imageId.jpg'; 182 | results.add({'url': imageUrl, 'imageId': imageId}); 183 | } else { 184 | // debugPrint("[IgdbService] Screenshot details fetched but no URL or imageId found for item: $s"); 185 | } 186 | } 187 | return results; 188 | } else { 189 | // debugPrint('[IgdbService] IGDB API error fetching screenshots: ${response.statusCode} ${response.body}'); 190 | } 191 | } catch (e) { 192 | // debugPrint('[IgdbService] Error fetching screenshots: $e'); 193 | } 194 | return []; 195 | } 196 | 197 | Future> fetchGameVideoIds(int gameId, Settings settings, String token) async { 198 | // debugPrint('[IgdbService] Fetching video IDs for game ID: $gameId'); 199 | try { 200 | final response = await http.post( 201 | Uri.parse(settings.igdbApiBaseUrl).replace(path: '/v4/game_videos'), // Use replace 202 | headers: { 203 | 'Accept': 'application/json', 204 | 'Client-ID': globalIgdbClientId, // Use global constant 205 | 'Authorization': 'Bearer $token', 206 | }, 207 | body: 'fields video_id; where game = $gameId;', 208 | ); 209 | 210 | if (response.statusCode == 200) { 211 | final List videos = json.decode(response.body); 212 | // debugPrint('[IgdbService] Video API Response Data: $videos'); 213 | return videos.map((v) => v['video_id'].toString()).toList(); 214 | } else { 215 | // debugPrint('[IgdbService] IGDB API error fetching videos: ${response.statusCode} ${response.body}'); 216 | } 217 | } catch (e) { 218 | // debugPrint('[IgdbService] Exception fetching game videos: $e'); 219 | } 220 | return []; 221 | } 222 | 223 | // Method to check and validate IGDB credentials 224 | Future validateCredentials(BuildContext context) async { 225 | final settingsProvider = Provider.of(context, listen: false); 226 | final settings = settingsProvider.settings; // Settings object no longer has clientID/secret 227 | 228 | // Check if global credentials are set (they should be, but good for robustness) 229 | if (globalIgdbClientId.isEmpty || globalIgdbClientSecret.isEmpty) { 230 | return false; 231 | } 232 | 233 | try { 234 | final tokenData = await getIgdbToken(settings); 235 | if (tokenData != null && tokenData['token'] != null) { 236 | // If token is new, update it in settings 237 | if (tokenData['isNew'] == true) { 238 | await settingsProvider.updateIgdbToken( 239 | tokenData['token'], 240 | tokenData['expiry'].difference(DateTime.now()), 241 | ); 242 | } 243 | return true; 244 | } 245 | } catch (e) { 246 | // Token retrieval failed 247 | return false; 248 | } 249 | 250 | return false; 251 | } 252 | } -------------------------------------------------------------------------------- /lib/services/log_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | // import 'package:flutter/foundation.dart'; // Unused import removed 3 | import 'package:path_provider/path_provider.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | class LogService { 7 | // Singleton instance 8 | static final LogService _instance = LogService._internal(); 9 | factory LogService() => _instance; 10 | LogService._internal(); 11 | 12 | // In-memory log storage 13 | final List _logs = []; 14 | 15 | // Maximum number of logs to keep in memory 16 | final int maxLogsInMemory = 1000; 17 | 18 | // File path for persistent logs 19 | String? _logFilePath; 20 | 21 | // Date formatter for timestamps 22 | final DateFormat _dateFormatter = DateFormat('yyyy-MM-dd HH:mm:ss'); 23 | 24 | // Initialize log service 25 | Future initialize() async { 26 | try { 27 | final appDir = await getApplicationSupportDirectory(); 28 | final logsDir = Directory('${appDir.path}/logs'); 29 | 30 | if (!await logsDir.exists()) { 31 | await logsDir.create(recursive: true); 32 | } 33 | 34 | final now = DateTime.now(); 35 | final dateString = DateFormat('yyyy-MM-dd').format(now); 36 | _logFilePath = '${logsDir.path}/app_log_$dateString.log'; 37 | 38 | // Log initialization *after* setting the path, so this message goes to the file too 39 | log('LogService initialized. Log file: $_logFilePath', LogLevel.info); 40 | } catch (e) { 41 | // debugPrint('Failed to initialize LogService: $e'); 42 | log('Failed to initialize LogService: $e', LogLevel.error); // Also log the error 43 | } 44 | } 45 | 46 | // Add a log entry 47 | void log(String message, [LogLevel level = LogLevel.info]) { 48 | final timestamp = DateTime.now(); 49 | final entry = LogEntry( 50 | message: message, 51 | timestamp: timestamp, 52 | level: level, 53 | ); 54 | 55 | // Add to in-memory logs 56 | _logs.insert(0, entry); // Add at the beginning (newest first) 57 | 58 | // Trim if exceeding max size 59 | if (_logs.length > maxLogsInMemory) { 60 | _logs.removeLast(); 61 | } 62 | 63 | // Print to console 64 | // debugPrint('${_formatTimestamp(timestamp)} [${level.toString().split('.').last}] $message'); 65 | 66 | // Write to file asynchronously 67 | _writeToFile(entry); 68 | } 69 | 70 | // Get all logs 71 | List getLogs() { 72 | return List.unmodifiable(_logs); 73 | } 74 | 75 | // Clear logs 76 | void clearLogs() { 77 | _logs.clear(); 78 | log('Logs cleared', LogLevel.info); 79 | } 80 | 81 | // Format timestamp for display 82 | String _formatTimestamp(DateTime timestamp) { 83 | return _dateFormatter.format(timestamp); 84 | } 85 | 86 | // Write log entry to file 87 | Future _writeToFile(LogEntry entry) async { 88 | if (_logFilePath == null) return; 89 | 90 | try { 91 | final file = File(_logFilePath!); 92 | final formattedLog = '${_formatTimestamp(entry.timestamp)} [${entry.level.toString().split('.').last}] ${entry.message}\n'; 93 | 94 | await file.writeAsString( 95 | formattedLog, 96 | mode: FileMode.append, 97 | ); 98 | } catch (e) { 99 | // debugPrint('Failed to write log to file: $e'); 100 | } 101 | } 102 | } 103 | 104 | // Log entry model 105 | class LogEntry { 106 | final String message; 107 | final DateTime timestamp; 108 | final LogLevel level; 109 | 110 | LogEntry({ 111 | required this.message, 112 | required this.timestamp, 113 | this.level = LogLevel.info 114 | }); 115 | 116 | @override 117 | String toString() { 118 | final formatter = DateFormat('yyyy-MM-dd HH:mm:ss'); 119 | return '${formatter.format(timestamp)} [${level.toString().split('.').last}] $message'; 120 | } 121 | } 122 | 123 | // Log levels 124 | enum LogLevel { 125 | debug, 126 | info, 127 | warning, 128 | error, 129 | } 130 | -------------------------------------------------------------------------------- /lib/services/power_management_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'log_service.dart'; 3 | 4 | class PowerManagementService { 5 | final LogService _logService; 6 | Process? _inhibitProcess; 7 | bool _isInhibited = false; 8 | 9 | PowerManagementService(this._logService); 10 | 11 | /// Prevents system sleep/logout using systemd-inhibit 12 | Future inhibitSleep({String reason = 'Wine Prefix Manager operation'}) async { 13 | if (_isInhibited) { 14 | _logService.log('Sleep inhibit already active'); 15 | return true; 16 | } 17 | 18 | try { 19 | // Use systemd-inhibit to prevent sleep, shutdown, and idle 20 | _inhibitProcess = await Process.start('systemd-inhibit', [ 21 | '--what=sleep:shutdown:idle', 22 | '--who=Wine Prefix Manager', 23 | '--why=$reason', 24 | '--mode=block', 25 | 'sleep', 'infinity' 26 | ]); 27 | 28 | if (_inhibitProcess != null) { 29 | _isInhibited = true; 30 | _logService.log('Sleep/logout inhibited: $reason'); 31 | return true; 32 | } 33 | } catch (e) { 34 | // If systemd-inhibit is not available, try alternative methods 35 | _logService.log('systemd-inhibit not available, trying xset: $e', LogLevel.warning); 36 | return await _tryXsetInhibit(); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /// Alternative method using xset to prevent screen sleep 43 | Future _tryXsetInhibit() async { 44 | try { 45 | // Disable DPMS (Display Power Management Signaling) 46 | final result = await Process.run('xset', ['s', 'off', '-dpms']); 47 | if (result.exitCode == 0) { 48 | _isInhibited = true; 49 | _logService.log('Screen sleep disabled using xset'); 50 | return true; 51 | } 52 | } catch (e) { 53 | _logService.log('xset command failed: $e', LogLevel.warning); 54 | } 55 | return false; 56 | } 57 | 58 | /// Allows system sleep/logout by stopping the inhibit process 59 | Future allowSleep() async { 60 | if (!_isInhibited) { 61 | _logService.log('Sleep inhibit is not active'); 62 | return; 63 | } 64 | 65 | try { 66 | // Stop the systemd-inhibit process 67 | if (_inhibitProcess != null) { 68 | _inhibitProcess!.kill(); 69 | await _inhibitProcess!.exitCode; 70 | _inhibitProcess = null; 71 | _logService.log('Sleep/logout inhibit removed'); 72 | } else { 73 | // Re-enable DPMS if we used xset 74 | await Process.run('xset', ['s', 'on', '+dpms']); 75 | _logService.log('Screen sleep re-enabled using xset'); 76 | } 77 | 78 | _isInhibited = false; 79 | } catch (e) { 80 | _logService.log('Error removing sleep inhibit: $e', LogLevel.error); 81 | } 82 | } 83 | 84 | /// Check if sleep is currently inhibited 85 | bool get isInhibited => _isInhibited; 86 | 87 | /// Cleanup - ensure we allow sleep when service is disposed 88 | Future dispose() async { 89 | await allowSleep(); 90 | } 91 | } -------------------------------------------------------------------------------- /lib/services/prefix_creation_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:process_run/shell.dart'; 4 | import '../models/settings.dart'; 5 | import '../models/wine_build.dart'; 6 | import '../models/prefix_models.dart'; 7 | import 'log_service.dart'; 8 | import 'wine_prefix_creation_service.dart'; 9 | import 'proton_prefix_creation_service.dart'; 10 | 11 | typedef StatusCallback = void Function(String status); 12 | typedef ProgressCallback = void Function(double progress); // Progress 0.0 to 1.0 13 | 14 | /// Main prefix creation service that delegates to specialized services based on prefix type 15 | class PrefixCreationService { 16 | final Dio _dio = Dio(); 17 | final Shell _shell = Shell(verbose: false); 18 | final LogService _logService = LogService(); 19 | 20 | // Specialized services 21 | final WinePrefixCreationService _wineService = WinePrefixCreationService(); 22 | final ProtonPrefixCreationService _protonService = ProtonPrefixCreationService(); 23 | 24 | /// Creates a prefix of the specified type 25 | Future downloadAndCreatePrefix({ 26 | required BaseBuild? selectedBuild, 27 | required String prefixName, 28 | required Settings settings, 29 | required PrefixType prefixType, 30 | required String architecture, 31 | required StatusCallback onStatusUpdate, 32 | required ProgressCallback onProgressUpdate, 33 | }) async { 34 | // Log what we're doing 35 | _logService.log('Creating ${prefixType.name} prefix: $prefixName with architecture $architecture'); 36 | 37 | // Delegate to the appropriate specialized service based on prefix type 38 | switch (prefixType) { 39 | case PrefixType.wine: 40 | return _wineService.createWinePrefix( 41 | selectedBuild: selectedBuild, 42 | prefixName: prefixName, 43 | settings: settings, 44 | architecture: architecture, 45 | onStatusUpdate: onStatusUpdate, 46 | onProgressUpdate: onProgressUpdate, 47 | ); 48 | 49 | case PrefixType.proton: 50 | return _protonService.createProtonPrefix( 51 | selectedBuild: selectedBuild, 52 | prefixName: prefixName, 53 | settings: settings, 54 | architecture: architecture, 55 | onStatusUpdate: onStatusUpdate, 56 | onProgressUpdate: onProgressUpdate, 57 | ); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /lib/services/prefix_creator.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/lib/services/prefix_creator.dart -------------------------------------------------------------------------------- /lib/services/prefix_storage_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:path/path.dart' as path; // Import path package 4 | import '../models/prefix_models.dart'; 5 | import '../models/settings.dart'; // Import Settings model 6 | import '../utils/path_utils.dart'; // Import the new utility 7 | 8 | class PrefixStorageService { 9 | static const String _appName = 'wine_prefix_manager'; // Match AppSettings 10 | static const String _defaultGameLibraryFileName = 'game_library.json'; // Match AppSettings 11 | 12 | /// Determines the path for the prefix data file. 13 | /// Uses the path from settings if provided, otherwise defaults to the app data directory. 14 | Future _getConfigPath(Settings settings) async { 15 | // Use custom path if provided and not empty 16 | if (settings.gameLibraryPath != null && settings.gameLibraryPath!.isNotEmpty) { 17 | try { 18 | final dir = Directory(path.dirname(settings.gameLibraryPath!)); 19 | if (!await dir.exists()) { 20 | await dir.create(recursive: true); 21 | } 22 | return settings.gameLibraryPath!; 23 | } catch (e) { 24 | print('Warning: Could not create directory for custom game library path: ${settings.gameLibraryPath}. Error: $e'); 25 | // Fallback to default path if directory creation for custom path fails 26 | return _getDefaultConfigPath(); 27 | } 28 | } 29 | // Fallback to default path 30 | return _getDefaultConfigPath(); 31 | } 32 | 33 | /// Gets the default configuration path in the application's data directory. 34 | Future _getDefaultConfigPath() async { 35 | final baseAppDataPath = await getBaseAppDataPath(); // Use imported function 36 | final gameLibPath = path.join(baseAppDataPath, _defaultGameLibraryFileName); 37 | // Ensure the directory exists when getting the default path too 38 | try { 39 | final dir = Directory(path.dirname(gameLibPath)); 40 | if (!await dir.exists()) { 41 | await dir.create(recursive: true); 42 | } 43 | } catch (e) { 44 | print('Warning: Could not create directory for default game library path: $gameLibPath. Error: $e'); 45 | // If it fails, it will likely fail on write later, but we try to be proactive. 46 | } 47 | return gameLibPath; 48 | } 49 | 50 | /// Loads prefixes from the configured path. 51 | Future> loadPrefixes(Settings settings) async { // Accept Settings 52 | try { 53 | final filePath = await _getConfigPath(settings); // Pass settings 54 | // Loading prefixes from: $filePath 55 | final file = File(filePath); 56 | if (await file.exists()) { 57 | final content = await file.readAsString(); 58 | if (content.trim().isEmpty) { 59 | // Handle empty file case 60 | return []; 61 | } 62 | final List jsonList = jsonDecode(content); 63 | return jsonList.map((p) => WinePrefix.fromJson(p)).toList(); 64 | } else { 65 | // Prefix file not found at: $filePath 66 | } 67 | } catch (e) { 68 | // Error loading prefixes 69 | // Depending on requirements, might rethrow, return empty list, or handle differently 70 | } 71 | return []; // Return empty list if file doesn't exist or on error 72 | } 73 | 74 | /// Saves prefixes to the configured path. 75 | Future savePrefixes(List prefixes, Settings settings) async { // Accept Settings 76 | try { 77 | final filePath = await _getConfigPath(settings); // Pass settings 78 | // Saving prefixes to: $filePath 79 | final file = File(filePath); 80 | // Ensure the directory exists before writing 81 | final dir = file.parent; 82 | if (!await dir.exists()) { 83 | await dir.create(recursive: true); 84 | // Created directory for saving prefixes 85 | } 86 | final jsonString = jsonEncode(prefixes.map((p) => p.toJson()).toList()); 87 | await file.writeAsString(jsonString); 88 | } catch (e) { 89 | // Error saving prefixes 90 | // Rethrow or handle as appropriate 91 | rethrow; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /lib/services/winecfg_32bit_helper.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/lib/services/winecfg_32bit_helper.dart -------------------------------------------------------------------------------- /lib/theme/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | import 'package:path/path.dart' as path; 5 | 6 | class ThemeProvider extends ChangeNotifier { 7 | bool _isDarkMode = false; 8 | 9 | bool get isDarkMode => _isDarkMode; 10 | ThemeData get themeData => _isDarkMode ? _darkTheme : _lightTheme; 11 | 12 | ThemeProvider() { 13 | _loadPrefs(); 14 | } 15 | 16 | Future _loadPrefs() async { 17 | final file = _getSettingsFile(); 18 | try { 19 | if (await file.exists()) { 20 | final contents = await file.readAsString(); 21 | final data = jsonDecode(contents); 22 | _isDarkMode = data['darkMode'] ?? false; 23 | notifyListeners(); 24 | } 25 | } catch (e) { 26 | // Error loading theme settings 27 | } 28 | } 29 | 30 | Future toggleTheme() async { 31 | _isDarkMode = !_isDarkMode; 32 | notifyListeners(); 33 | 34 | try { 35 | final file = _getSettingsFile(); 36 | await file.writeAsString(jsonEncode({'darkMode': _isDarkMode})); 37 | } catch (e) { 38 | // Error saving theme settings 39 | } 40 | } 41 | 42 | File _getSettingsFile() { 43 | final homeDir = Platform.environment['HOME']!; 44 | return File(path.join(homeDir, '.wine_prefix_manager_theme.json')); 45 | } 46 | 47 | // Light theme 48 | static final _lightTheme = ThemeData( 49 | brightness: Brightness.light, 50 | useMaterial3: true, 51 | colorScheme: ColorScheme.fromSeed( 52 | seedColor: Colors.deepPurple, 53 | brightness: Brightness.light, 54 | ), 55 | ); 56 | 57 | // Dark theme 58 | static final _darkTheme = ThemeData( 59 | brightness: Brightness.dark, 60 | useMaterial3: true, 61 | colorScheme: ColorScheme.fromSeed( 62 | seedColor: Colors.deepPurple, 63 | brightness: Brightness.dark, 64 | ), 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /lib/utils/path_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as path; 3 | 4 | // appNameForPaths is not strictly needed here if we use a fixed folder name like wpm_settings 5 | // const String appNameForPaths = 'wine_prefix_manager'; 6 | const String targetAppDataFolderName = 'wpm_settings'; 7 | 8 | Future getBaseAppDataPath() async { 9 | final homeDir = Platform.environment['HOME']; 10 | if (homeDir == null || homeDir.isEmpty) { 11 | print('Warning: HOME environment variable not set. Using temporary directory for app data.'); 12 | // Fallback to a temporary directory, less ideal for persistent data. 13 | final tempDir = await Directory.systemTemp.createTemp(targetAppDataFolderName); 14 | return tempDir.path; 15 | } 16 | // New preferred path: ~/wpm_settings/ 17 | return path.join(homeDir, targetAppDataFolderName); 18 | } -------------------------------------------------------------------------------- /lib/widgets/action_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ActionButton extends StatelessWidget { 4 | final String label; 5 | final IconData icon; 6 | final VoidCallback onPressed; 7 | final String? tooltip; 8 | final bool isDestructive; 9 | 10 | const ActionButton({ 11 | Key? key, 12 | required this.label, 13 | required this.icon, 14 | required this.onPressed, 15 | this.tooltip, 16 | this.isDestructive = false, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final buttonStyle = isDestructive 22 | ? ElevatedButton.styleFrom( 23 | backgroundColor: Theme.of(context).colorScheme.errorContainer, 24 | foregroundColor: Theme.of(context).colorScheme.onErrorContainer, 25 | ) 26 | : ElevatedButton.styleFrom( 27 | backgroundColor: Theme.of(context).colorScheme.secondaryContainer, 28 | foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer, 29 | ); 30 | 31 | final button = ElevatedButton.icon( 32 | onPressed: onPressed, 33 | icon: Icon(icon, size: 18), // Adjusted icon size for consistency 34 | label: Text(label), 35 | style: buttonStyle.copyWith( 36 | padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)), // Adjusted padding 37 | ) 38 | ); 39 | 40 | if (tooltip != null) { 41 | return Tooltip( 42 | message: tooltip!, 43 | child: button, 44 | ); 45 | } 46 | return button; 47 | } 48 | } -------------------------------------------------------------------------------- /lib/widgets/change_prefix_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/prefix_models.dart'; 3 | 4 | class ChangePrefixDialog extends StatefulWidget { 5 | final GameEntry gameEntry; 6 | final List allPrefixes; 7 | final Function(WinePrefix) onPrefixSelected; // Callback when confirmed 8 | 9 | const ChangePrefixDialog({ 10 | Key? key, 11 | required this.gameEntry, 12 | required this.allPrefixes, 13 | required this.onPrefixSelected, 14 | }) : super(key: key); 15 | 16 | @override 17 | State createState() => _ChangePrefixDialogState(); 18 | } 19 | 20 | class _ChangePrefixDialogState extends State { 21 | WinePrefix? _selectedDestinationPrefix; 22 | late List _availableDestinations; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | // Filter out the current prefix 28 | _availableDestinations = widget.allPrefixes 29 | .where((p) => p.path != widget.gameEntry.prefix.path) 30 | .toList(); 31 | // Sort for consistent order 32 | _availableDestinations.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); 33 | // Pre-select the first available destination if any 34 | if (_availableDestinations.isNotEmpty) { 35 | _selectedDestinationPrefix = _availableDestinations.first; 36 | } 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return AlertDialog( 42 | title: Text('Change Prefix for "${widget.gameEntry.exe.name}"'), 43 | content: SizedBox( 44 | width: 400, // Give the dialog some width 45 | child: Column( 46 | mainAxisSize: MainAxisSize.min, 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | children: [ 49 | Text('Current Prefix: ${widget.gameEntry.prefix.name}'), 50 | const SizedBox(height: 20), 51 | if (_availableDestinations.isEmpty) 52 | const Text('No other prefixes available to move to.') 53 | else ...[ 54 | const Text('Select New Prefix:'), 55 | const SizedBox(height: 8), 56 | // Use DropdownButton for selection 57 | DropdownButton( 58 | value: _selectedDestinationPrefix, 59 | isExpanded: true, 60 | hint: const Text('Select a prefix'), 61 | items: _availableDestinations.map((prefix) { 62 | return DropdownMenuItem( 63 | value: prefix, 64 | child: Text(prefix.name), 65 | ); 66 | }).toList(), 67 | onChanged: (WinePrefix? newValue) { 68 | setState(() { 69 | _selectedDestinationPrefix = newValue; 70 | }); 71 | }, 72 | ), 73 | ], 74 | ], 75 | ), 76 | ), 77 | actions: [ 78 | TextButton( 79 | onPressed: () => Navigator.of(context).pop(), 80 | child: const Text('Cancel'), 81 | ), 82 | FilledButton( 83 | onPressed: (_selectedDestinationPrefix == null) 84 | ? null // Disable if no destination is selected or available 85 | : () { 86 | widget.onPrefixSelected(_selectedDestinationPrefix!); 87 | Navigator.of(context).pop(); // Close this dialog 88 | }, 89 | child: const Text('Confirm Change'), 90 | ), 91 | ], 92 | ); 93 | } 94 | } -------------------------------------------------------------------------------- /lib/widgets/custom_title_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:window_manager/window_manager.dart'; 3 | 4 | class CustomTitleBar extends StatelessWidget implements PreferredSizeWidget { 5 | // final String title; // Removed title parameter 6 | final bool isConnected; // Add connectivity status 7 | final Color? backgroundColor; // Optional background color 8 | 9 | const CustomTitleBar({ 10 | super.key, 11 | // required this.title, // Removed title parameter 12 | required this.isConnected, // Make it required 13 | this.backgroundColor, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final theme = Theme.of(context); 19 | final titleBarColor = backgroundColor ?? theme.appBarTheme.backgroundColor ?? theme.colorScheme.primary; 20 | final iconColor = theme.colorScheme.onPrimary; // Adjust if needed based on titleBarColor contrast 21 | 22 | return GestureDetector( 23 | onPanStart: (details) { 24 | windowManager.startDragging(); 25 | }, 26 | child: Container( 27 | height: preferredSize.height, 28 | color: titleBarColor, 29 | child: Row( 30 | children: [ 31 | // Optional: Add an icon or padding at the start 32 | const SizedBox(width: 16), 33 | // Title Text Widget Removed 34 | // Expanded( 35 | // child: Text( 36 | // title, 37 | // style: theme.textTheme.titleMedium?.copyWith(color: iconColor), 38 | // overflow: TextOverflow.ellipsis, 39 | // ), 40 | // ), 41 | // Spacer to push buttons to the right 42 | const Spacer(), 43 | // Connectivity Indicator 44 | Padding( 45 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 46 | child: Icon( 47 | isConnected ? Icons.wifi : Icons.wifi_off, 48 | color: isConnected ? iconColor : Colors.orangeAccent, // Different color when offline 49 | size: 18, // Smaller icon size 50 | ), 51 | ), 52 | // Window Control Buttons 53 | MinimizeButton(color: iconColor), 54 | MaximizeRestoreButton(color: iconColor), 55 | CloseButton(color: iconColor), 56 | ], 57 | ), 58 | ), 59 | ); 60 | } 61 | 62 | @override 63 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); // Standard AppBar height 64 | } 65 | 66 | // --- Button Widgets --- 67 | 68 | class MinimizeButton extends StatelessWidget { 69 | final Color? color; 70 | const MinimizeButton({super.key, this.color}); 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return IconButton( 75 | icon: const Icon(Icons.minimize), 76 | color: color, 77 | tooltip: 'Minimize', 78 | onPressed: () => windowManager.minimize(), 79 | ); 80 | } 81 | } 82 | 83 | class MaximizeRestoreButton extends StatefulWidget { 84 | final Color? color; 85 | const MaximizeRestoreButton({super.key, this.color}); 86 | 87 | @override 88 | State createState() => _MaximizeRestoreButtonState(); 89 | } 90 | 91 | class _MaximizeRestoreButtonState extends State { 92 | bool _isMaximized = false; 93 | 94 | @override 95 | void initState() { 96 | super.initState(); 97 | // Listen to maximize/unmaximize events to update the icon 98 | windowManager.isMaximized().then((value) { 99 | if (mounted) { 100 | setState(() { 101 | _isMaximized = value; 102 | }); 103 | } 104 | }); 105 | // Add listener (consider using WindowListener mixin for more robust handling) 106 | // This basic approach might miss some edge cases. 107 | // A more robust solution would involve implementing WindowListener. 108 | } 109 | 110 | @override 111 | Widget build(BuildContext context) { 112 | return IconButton( 113 | icon: Icon(_isMaximized ? Icons.fullscreen_exit : Icons.fullscreen), 114 | color: widget.color, 115 | tooltip: _isMaximized ? 'Restore' : 'Maximize', 116 | onPressed: () async { 117 | bool isMax = await windowManager.isMaximized(); 118 | if (isMax) { 119 | await windowManager.unmaximize(); 120 | } else { 121 | await windowManager.maximize(); 122 | } 123 | // Update state after action 124 | if (mounted) { 125 | setState(() { 126 | _isMaximized = !isMax; 127 | }); 128 | } 129 | }, 130 | ); 131 | } 132 | } 133 | 134 | 135 | class CloseButton extends StatelessWidget { 136 | final Color? color; 137 | const CloseButton({super.key, this.color}); 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | return IconButton( 142 | icon: const Icon(Icons.close), 143 | color: color, 144 | tooltip: 'Close', 145 | onPressed: () => windowManager.close(), 146 | ); 147 | } 148 | } -------------------------------------------------------------------------------- /lib/widgets/executable_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/prefix_models.dart'; 3 | import 'common_components_dialog.dart'; // Import for confirmation dialog 4 | 5 | // Define callback types required by this widget 6 | typedef ExeActionCallback = void Function(WinePrefix prefix, ExeEntry exe); 7 | typedef ExeContextActionCallback = void Function(BuildContext context, WinePrefix prefix, ExeEntry exe); 8 | 9 | class ExecutableListTile extends StatelessWidget { 10 | final WinePrefix prefix; 11 | final ExeEntry exe; 12 | final bool isRunning; 13 | final ExeActionCallback onRunExe; 14 | final ExeActionCallback onKillProcess; 15 | final ExeContextActionCallback onDeleteExe; // Needs context for dialog 16 | 17 | const ExecutableListTile({ 18 | Key? key, 19 | required this.prefix, 20 | required this.exe, 21 | required this.isRunning, 22 | required this.onRunExe, 23 | required this.onKillProcess, 24 | required this.onDeleteExe, 25 | }) : super(key: key); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return ListTile( 30 | leading: Icon(exe.isGame ? Icons.sports_esports : Icons.apps), 31 | title: Text(exe.name), 32 | subtitle: Text( 33 | exe.path, 34 | maxLines: 1, 35 | overflow: TextOverflow.ellipsis, 36 | style: Theme.of(context).textTheme.bodySmall, 37 | ), 38 | trailing: Row( 39 | mainAxisSize: MainAxisSize.min, 40 | children: [ 41 | IconButton( 42 | icon: Icon(isRunning ? Icons.stop_circle_outlined : Icons.play_circle_outline), 43 | tooltip: isRunning ? 'Stop' : 'Run', 44 | color: isRunning ? Colors.red : Colors.green, 45 | onPressed: () => isRunning ? onKillProcess(prefix, exe) : onRunExe(prefix, exe), 46 | ), 47 | IconButton( 48 | icon: const Icon(Icons.delete), 49 | tooltip: 'Delete Executable', 50 | onPressed: () { // Modified onPressed for confirmation 51 | showConfirmationDialog( 52 | context: context, 53 | title: 'Delete Executable?', 54 | content: Text('Are you sure you want to remove the executable "${exe.name}" from the prefix "${prefix.name}"?\n\nThis only removes the entry from the manager, it does not delete the actual file.'), 55 | confirmButtonText: 'Delete', 56 | onConfirm: () => onDeleteExe(context, prefix, exe), // Call original callback on confirm 57 | ); 58 | }, 59 | ), 60 | // Consider adding an Edit button here later that calls _editGameDetails 61 | // IconButton( 62 | // icon: const Icon(Icons.edit), 63 | // tooltip: 'Edit Details', 64 | // onPressed: () { /* Call edit details callback */ }, 65 | // ), 66 | ], 67 | ), 68 | // Optional: Add onTap to edit details directly? 69 | // onTap: () { /* Call edit details callback */ }, 70 | ); 71 | } 72 | } -------------------------------------------------------------------------------- /lib/widgets/game_carousel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/prefix_models.dart'; 3 | import '../widgets/game_card.dart'; 4 | 5 | class GameCarousel extends StatelessWidget { 6 | final List games; 7 | final Function(GameEntry) onGameTap; 8 | final Function(BuildContext, GameEntry) onShowDetails; 9 | final Function(GameEntry) onLaunch; 10 | 11 | const GameCarousel({ 12 | Key? key, 13 | required this.games, 14 | required this.onGameTap, 15 | required this.onShowDetails, 16 | required this.onLaunch, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | if (games.isEmpty) { 22 | return const SizedBox( 23 | height: 200, 24 | child: Center( 25 | child: Text('No games found'), 26 | ), 27 | ); 28 | } 29 | 30 | return SizedBox( 31 | height: 220, 32 | child: ListView.builder( 33 | scrollDirection: Axis.horizontal, 34 | itemCount: games.length, 35 | padding: const EdgeInsets.symmetric(horizontal: 16), 36 | itemBuilder: (context, index) { 37 | final game = games[index]; 38 | return Padding( 39 | padding: const EdgeInsets.only(right: 16), 40 | child: SizedBox( 41 | width: 140, 42 | child: InkWell( 43 | onTap: () => onShowDetails(context, game), 44 | child: Column( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | ClipRRect( 48 | borderRadius: BorderRadius.circular(8), 49 | child: GameCard( 50 | game: game, 51 | onShowInfo: (game) => onShowDetails(context, game), 52 | onLaunch: onLaunch, 53 | ), 54 | ), 55 | const SizedBox(height: 8), 56 | Text( 57 | game.exe.name, 58 | maxLines: 1, 59 | overflow: TextOverflow.ellipsis, 60 | style: const TextStyle( 61 | fontWeight: FontWeight.bold, 62 | ), 63 | ), 64 | Text( 65 | game.prefix.name, 66 | maxLines: 1, 67 | overflow: TextOverflow.ellipsis, 68 | style: TextStyle( 69 | color: Theme.of(context).textTheme.bodySmall?.color, 70 | fontSize: 12, 71 | ), 72 | ), 73 | ], 74 | ), 75 | ), 76 | ), 77 | ); 78 | }, 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/widgets/game_search_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/igdb_models.dart'; 3 | 4 | class GameSearchDialog extends StatefulWidget { 5 | final String initialQuery; 6 | // Update the function signature to expect a Map 7 | final Future> Function(String query) onSearch; 8 | 9 | const GameSearchDialog({ 10 | Key? key, 11 | required this.initialQuery, 12 | required this.onSearch, 13 | }) : super(key: key); 14 | 15 | @override 16 | State createState() => _GameSearchDialogState(); 17 | } 18 | 19 | class _GameSearchDialogState extends State { 20 | late TextEditingController _controller; 21 | List _results = []; 22 | bool _isLoading = false; 23 | String _error = ''; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _controller = TextEditingController(text: widget.initialQuery); 29 | _search(); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | _controller.dispose(); 35 | super.dispose(); 36 | } 37 | 38 | Future _search() async { 39 | if (_controller.text.isEmpty) return; 40 | 41 | setState(() { 42 | _isLoading = true; 43 | _error = ''; 44 | _results = []; // Clear previous results 45 | }); 46 | 47 | // Call the updated onSearch which returns a Map 48 | final searchResult = await widget.onSearch(_controller.text); 49 | 50 | // Check if the widget is still mounted before calling setState 51 | if (!mounted) return; 52 | 53 | // Handle the result map 54 | if (searchResult.containsKey('error')) { 55 | setState(() { 56 | _isLoading = false; 57 | // Display a more user-friendly error message 58 | _error = 'Search failed: ${searchResult['error']}'; 59 | }); 60 | } else if (searchResult.containsKey('games')) { 61 | setState(() { 62 | _results = searchResult['games'] as List; 63 | _isLoading = false; 64 | if (_results.isEmpty) { 65 | _error = 'No results found for "${_controller.text}".'; 66 | } 67 | }); 68 | } else { 69 | // Handle unexpected map structure 70 | setState(() { 71 | _isLoading = false; 72 | _error = 'Unexpected search result format.'; 73 | }); 74 | } 75 | } 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return AlertDialog( 80 | title: const Text('Search Game on IGDB'), // Updated title 81 | content: SizedBox( 82 | width: double.maxFinite, 83 | // Consider making height dynamic or larger if needed 84 | height: MediaQuery.of(context).size.height * 0.6, // Use a portion of screen height 85 | child: Column( 86 | mainAxisSize: MainAxisSize.min, 87 | children: [ 88 | Row( 89 | children: [ 90 | Expanded( 91 | child: TextField( 92 | controller: _controller, 93 | decoration: InputDecoration( 94 | labelText: 'Game Name', 95 | hintText: 'Type game name to search', 96 | // Add clear button 97 | suffixIcon: _controller.text.isNotEmpty 98 | ? IconButton( 99 | icon: const Icon(Icons.clear), 100 | onPressed: () { 101 | _controller.clear(); 102 | setState(() { 103 | _results = []; 104 | _error = ''; 105 | }); 106 | }, 107 | ) 108 | : null, 109 | ), 110 | onSubmitted: (_) => _search(), 111 | autofocus: true, // Focus on load 112 | ), 113 | ), 114 | IconButton( 115 | icon: const Icon(Icons.search), 116 | tooltip: 'Search', // Add tooltip 117 | onPressed: _isLoading ? null : _search, // Disable while loading 118 | ), 119 | ], 120 | ), 121 | const SizedBox(height: 16), 122 | // Display error prominently if it exists 123 | if (_error.isNotEmpty && !_isLoading) 124 | Padding( 125 | padding: const EdgeInsets.symmetric(vertical: 16.0), 126 | child: Text( 127 | _error, 128 | style: TextStyle(color: Theme.of(context).colorScheme.error), 129 | textAlign: TextAlign.center, 130 | ), 131 | ), 132 | // Show loading indicator or results/no results message 133 | Expanded( 134 | child: _isLoading 135 | ? const Center(child: CircularProgressIndicator()) 136 | : _results.isNotEmpty 137 | ? ListView.builder( 138 | shrinkWrap: true, 139 | itemCount: _results.length, 140 | itemBuilder: (context, index) { 141 | final game = _results[index]; 142 | return ListTile( 143 | title: Text(game.name), 144 | subtitle: game.summary != null 145 | ? Text( 146 | game.summary!, 147 | maxLines: 2, 148 | overflow: TextOverflow.ellipsis, 149 | ) 150 | : null, 151 | onTap: () => Navigator.of(context).pop(game), 152 | ); 153 | }, 154 | ) 155 | // Only show 'No results' if there's no error message already shown 156 | : _error.isEmpty 157 | ? const Center(child: Text('Enter a search term.')) 158 | : const SizedBox.shrink(), // Hide if error is shown 159 | ), 160 | ], 161 | ), 162 | ), 163 | actions: [ 164 | TextButton( 165 | onPressed: () => Navigator.of(context).pop(null), 166 | child: const Text('Cancel'), 167 | ), 168 | ], 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/widgets/json_viewer_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; // For Clipboard 5 | 6 | class JsonViewerPage extends StatefulWidget { 7 | final String filePath; 8 | 9 | const JsonViewerPage({Key? key, required this.filePath}) : super(key: key); 10 | 11 | @override 12 | _JsonViewerPageState createState() => _JsonViewerPageState(); 13 | } 14 | 15 | class _JsonViewerPageState extends State { 16 | String _jsonContent = ''; 17 | bool _isLoading = true; 18 | String? _error; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _loadJsonContent(); 24 | } 25 | 26 | Future _loadJsonContent() async { 27 | if (!mounted) return; 28 | setState(() { 29 | _isLoading = true; 30 | _error = null; 31 | }); 32 | try { 33 | final file = File(widget.filePath); 34 | if (await file.exists()) { 35 | final content = await file.readAsString(); 36 | // Pretty print the JSON 37 | const jsonEncoder = JsonEncoder.withIndent(' '); 38 | final decodedJson = jsonDecode(content); 39 | _jsonContent = jsonEncoder.convert(decodedJson); 40 | } else { 41 | _error = 'File not found: ${widget.filePath}'; 42 | _jsonContent = ''; // Clear content if file not found 43 | } 44 | } catch (e) { 45 | _error = 'Error reading or parsing JSON file: $e'; 46 | _jsonContent = ''; // Clear content on error 47 | } finally { 48 | if (mounted) { 49 | setState(() { 50 | _isLoading = false; 51 | }); 52 | } 53 | } 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return AlertDialog( 59 | title: const Text('Game Library JSON Viewer'), 60 | content: SizedBox( 61 | width: MediaQuery.of(context).size.width * 0.8, // 80% of screen width 62 | height: MediaQuery.of(context).size.height * 0.7, // 70% of screen height 63 | child: _isLoading 64 | ? const Center(child: CircularProgressIndicator()) 65 | : _error != null 66 | ? Center( 67 | child: Column( 68 | mainAxisAlignment: MainAxisAlignment.center, 69 | children: [ 70 | Icon(Icons.error_outline, color: Colors.red, size: 48), 71 | const SizedBox(height: 16), 72 | Text( 73 | _error!, 74 | textAlign: TextAlign.center, 75 | style: const TextStyle(color: Colors.red), 76 | ), 77 | const SizedBox(height: 16), 78 | ElevatedButton.icon( 79 | icon: const Icon(Icons.refresh), 80 | label: const Text('Retry'), 81 | onPressed: _loadJsonContent, 82 | ), 83 | ], 84 | ), 85 | ) 86 | : _jsonContent.isEmpty && _error == null 87 | ? Center( 88 | child: Column( 89 | mainAxisAlignment: MainAxisAlignment.center, 90 | children: [ 91 | const Icon(Icons.info_outline, color: Colors.grey, size: 48), 92 | const SizedBox(height: 16), 93 | Text( 94 | 'The file is empty or does not exist at the specified path: ${widget.filePath}', 95 | textAlign: TextAlign.center, 96 | ), 97 | const SizedBox(height: 16), 98 | ElevatedButton.icon( 99 | icon: const Icon(Icons.refresh), 100 | label: const Text('Retry'), 101 | onPressed: _loadJsonContent, 102 | ), 103 | ], 104 | ), 105 | ) 106 | : SingleChildScrollView( 107 | padding: const EdgeInsets.all(8.0), 108 | child: SelectableText( 109 | _jsonContent, 110 | style: const TextStyle(fontFamily: 'monospace', fontSize: 12), 111 | ), 112 | ), 113 | ), 114 | actions: [ 115 | if (!_isLoading && _error == null && _jsonContent.isNotEmpty) 116 | TextButton.icon( 117 | icon: const Icon(Icons.copy), 118 | label: const Text('Copy to Clipboard'), 119 | onPressed: () { 120 | Clipboard.setData(ClipboardData(text: _jsonContent)); 121 | ScaffoldMessenger.of(context).showSnackBar( 122 | const SnackBar(content: Text('JSON content copied to clipboard')), 123 | ); 124 | }, 125 | ), 126 | TextButton( 127 | child: const Text('Close'), 128 | onPressed: () { 129 | Navigator.of(context).pop(); 130 | }, 131 | ), 132 | ], 133 | ); 134 | } 135 | } -------------------------------------------------------------------------------- /lib/widgets/prefix_creation_form_for_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/prefix_models.dart'; 3 | import '../models/settings.dart'; 4 | import 'prefix_creation_form.dart'; 5 | 6 | class PrefixCreationFormForType extends StatelessWidget { 7 | final Settings? settings; 8 | final PrefixType prefixType; 9 | 10 | const PrefixCreationFormForType({ 11 | Key? key, 12 | required this.settings, 13 | required this.prefixType, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | // Create the form with the specified prefix type 19 | return PrefixCreationForm( 20 | settings: settings, 21 | initialPrefixType: prefixType, 22 | ); 23 | } 24 | } -------------------------------------------------------------------------------- /lib/widgets/prefix_detail_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/prefix_models.dart'; 3 | import '../models/settings.dart'; // Needed for Common Components Dialog 4 | import 'common_components_dialog.dart'; // Import for confirmation dialog 5 | import 'action_button.dart'; // Import the new ActionButton widget 6 | 7 | // Define callback types required by this widget 8 | typedef PrefixActionCallback = void Function(WinePrefix prefix); 9 | typedef PrefixContextActionCallback = void Function(BuildContext context, WinePrefix prefix); 10 | // typedef PrefixSettingsActionCallback = void Function(BuildContext context, WinePrefix prefix, Settings settings); // No longer needed 11 | 12 | class PrefixDetailActions extends StatelessWidget { 13 | final WinePrefix prefix; 14 | final Settings? settings; // Make settings nullable, check before use 15 | final PrefixActionCallback onAddExecutable; 16 | final PrefixContextActionCallback onRunWinecfg; // Needs context 17 | final PrefixActionCallback onRunWinetricksGui; // No context needed here 18 | // final PrefixContextActionCallback onShowWinetricksVerbs; // Removed 19 | final PrefixContextActionCallback onShowCommonComponents; // FIX: Changed type 20 | final PrefixActionCallback onRunInstaller; 21 | final PrefixActionCallback onExploreHostFiles; // Renamed callback for clarity 22 | final PrefixContextActionCallback onDeletePrefix; // Needs context 23 | final PrefixContextActionCallback onRenamePrefix; // Added callback for rename 24 | final PrefixContextActionCallback onApplyControllerFix; // Added callback for controller fix 25 | final PrefixContextActionCallback onEditEnvVariables; // Added callback for environment variables 26 | 27 | const PrefixDetailActions({ 28 | Key? key, 29 | required this.prefix, 30 | required this.settings, 31 | required this.onAddExecutable, 32 | required this.onRunWinecfg, 33 | required this.onRunWinetricksGui, 34 | // required this.onShowWinetricksVerbs, // Removed 35 | required this.onShowCommonComponents, // FIX: Changed type 36 | required this.onRunInstaller, 37 | required this.onExploreHostFiles, // Renamed callback 38 | required this.onDeletePrefix, 39 | required this.onRenamePrefix, // Added rename callback 40 | required this.onApplyControllerFix, // Added controller fix callback 41 | required this.onEditEnvVariables, // Added env variables callback 42 | }) : super(key: key); 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final bool isProton = prefix.type == PrefixType.proton; 47 | // Determine if DXVK/VKD3D can be installed/reinstalled 48 | // bool canInstallGraphics = prefix.type == PrefixType.wine || prefix.wineBuildPath?.contains('Kronek') == true; 49 | 50 | return Padding( 51 | padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0, top: 8.0), 52 | child: Wrap( 53 | spacing: 8.0, // Horizontal spacing between chips 54 | runSpacing: 4.0, // Vertical spacing between lines of chips 55 | children: [ 56 | ActionButton( 57 | icon: Icons.add_to_photos_outlined, 58 | label: 'Add Executable', 59 | onPressed: () => onAddExecutable(prefix), 60 | ), 61 | ActionButton( 62 | icon: Icons.construction_outlined, 63 | label: 'Winetricks GUI', 64 | onPressed: () => onRunWinetricksGui(prefix), 65 | ), 66 | ActionButton( 67 | icon: Icons.settings_applications_outlined, 68 | label: 'Winecfg', 69 | onPressed: () => onRunWinecfg(context, prefix), 70 | ), 71 | // ActionButton( 72 | // icon: Icons.build_circle_outlined, 73 | // label: 'Winetricks Verbs', 74 | // onPressed: () => onShowWinetricksVerbs(context, prefix, settings!), 75 | // ), // Removed 76 | // ActionButton( 77 | // icon: Icons.extension_outlined, 78 | // label: 'Common Components', 79 | // onPressed: () => onShowCommonComponents(context, prefix), 80 | // ), // Removed Common Components button 81 | ActionButton( 82 | icon: Icons.rule_folder_outlined, // Changed icon to be more generic 83 | label: 'Install Software', 84 | tooltip: 'Run an installer (e.g., setup.exe) in this prefix', 85 | onPressed: () => onRunInstaller(prefix), 86 | ), 87 | ActionButton( 88 | icon: Icons.gamepad_outlined, 89 | label: 'Controller Fix', 90 | tooltip: 'Apply common controller fixes (XInput, DInput)', 91 | onPressed: () => onApplyControllerFix(context, prefix), 92 | ), 93 | ActionButton( 94 | icon: Icons.edit_note_outlined, 95 | label: 'Env Variables', 96 | tooltip: 'Edit environment variables for this prefix', 97 | onPressed: () => onEditEnvVariables(context, prefix), 98 | ), 99 | ActionButton( 100 | icon: Icons.folder_open_outlined, 101 | label: isProton ? 'Explore C: Drive' : 'Explore Files', 102 | tooltip: isProton ? 'Open the C: drive of this Proton prefix' : 'Explore host files mapped to this prefix', 103 | onPressed: () => onExploreHostFiles(prefix), 104 | ), 105 | ActionButton( 106 | icon: Icons.edit_outlined, 107 | label: 'Rename Prefix', 108 | onPressed: () => onRenamePrefix(context, prefix), 109 | isDestructive: false, 110 | ), 111 | ActionButton( 112 | icon: Icons.delete_forever_outlined, 113 | label: 'Delete Prefix', 114 | onPressed: () => onDeletePrefix(context, prefix), 115 | isDestructive: true, 116 | ), 117 | ], 118 | ), 119 | ); 120 | } 121 | } -------------------------------------------------------------------------------- /lib/widgets/prefix_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/prefix_models.dart'; 3 | 4 | class PrefixListTile extends StatelessWidget { 5 | final WinePrefix prefix; 6 | final VoidCallback? onTap; // Optional callback for tapping the tile itself 7 | 8 | const PrefixListTile({ 9 | Key? key, 10 | required this.prefix, 11 | this.onTap, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return ListTile( 17 | leading: Icon( 18 | prefix.type == PrefixType.proton ? Icons.games : Icons.wine_bar, 19 | color: Theme.of(context).colorScheme.primary, 20 | size: 36, // Slightly larger icon 21 | ), 22 | title: Text(prefix.name, style: const TextStyle(fontWeight: FontWeight.bold)), 23 | subtitle: Text( 24 | prefix.path, 25 | maxLines: 1, 26 | overflow: TextOverflow.ellipsis, 27 | style: Theme.of(context).textTheme.bodySmall, 28 | ), 29 | onTap: onTap, // Allow tapping the tile for potential future actions 30 | // Trailing actions (like delete, explore) will be inside the ExpansionTile content 31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /lib/widgets/rename_prefix_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import '../models/prefix_models.dart'; 4 | import '../providers/prefix_provider.dart'; // To check for existing names 5 | 6 | class RenamePrefixDialog extends StatefulWidget { 7 | final WinePrefix prefixToRename; 8 | final Function(String newName) onConfirmRename; 9 | 10 | const RenamePrefixDialog({ 11 | Key? key, 12 | required this.prefixToRename, 13 | required this.onConfirmRename, 14 | }) : super(key: key); 15 | 16 | @override 17 | State createState() => _RenamePrefixDialogState(); 18 | } 19 | 20 | class _RenamePrefixDialogState extends State { 21 | late TextEditingController _nameController; 22 | final _formKey = GlobalKey(); 23 | String? _errorMessage; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _nameController = TextEditingController(text: widget.prefixToRename.name); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | _nameController.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | String? _validateName(String? value) { 38 | if (value == null || value.trim().isEmpty) { 39 | return 'Prefix name cannot be empty.'; 40 | } 41 | if (value.trim() == widget.prefixToRename.name) { 42 | return 'New name must be different from the current name.'; 43 | } 44 | // Check for invalid characters (e.g., slashes) 45 | if (value.contains('/') || value.contains('\\')) { 46 | return 'Prefix name cannot contain slashes.'; 47 | } 48 | // Check if name already exists (using Provider) 49 | final prefixProvider = Provider.of(context, listen: false); 50 | if (prefixProvider.prefixes.any((p) => p.name == value.trim())) { 51 | return 'Prefix name "$value" already exists.'; 52 | } 53 | return null; 54 | } 55 | 56 | void _submitForm() { 57 | setState(() { 58 | _errorMessage = null; // Clear previous error 59 | }); 60 | if (_formKey.currentState!.validate()) { 61 | final newName = _nameController.text.trim(); 62 | try { 63 | widget.onConfirmRename(newName); 64 | Navigator.of(context).pop(); // Close dialog on success 65 | } catch (e) { 66 | // Handle potential errors from the rename logic (e.g., file system error) 67 | setState(() { 68 | _errorMessage = 'Error renaming prefix: $e'; 69 | }); 70 | } 71 | } 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return AlertDialog( 77 | title: Text('Rename Prefix "${widget.prefixToRename.name}"'), 78 | content: Form( 79 | key: _formKey, 80 | child: Column( 81 | mainAxisSize: MainAxisSize.min, 82 | children: [ 83 | TextFormField( 84 | controller: _nameController, 85 | autofocus: true, 86 | decoration: const InputDecoration( 87 | labelText: 'New Prefix Name', 88 | border: OutlineInputBorder(), 89 | ), 90 | validator: _validateName, 91 | onFieldSubmitted: (_) => _submitForm(), // Allow submitting with Enter key 92 | ), 93 | if (_errorMessage != null) ...[ 94 | const SizedBox(height: 16), 95 | Text( 96 | _errorMessage!, 97 | style: TextStyle(color: Theme.of(context).colorScheme.error), 98 | ), 99 | ] 100 | ], 101 | ), 102 | ), 103 | actions: [ 104 | TextButton( 105 | onPressed: () => Navigator.of(context).pop(), 106 | child: const Text('Cancel'), 107 | ), 108 | FilledButton( 109 | onPressed: _submitForm, 110 | child: const Text('Rename'), 111 | ), 112 | ], 113 | ); 114 | } 115 | } -------------------------------------------------------------------------------- /lib/widgets/text_input_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TextInputDialog extends StatefulWidget { 4 | final String title; 5 | final String labelText; 6 | final String? initialValue; 7 | final String confirmButtonText; 8 | final String? Function(String?)? validator; // Optional validator 9 | 10 | const TextInputDialog({ 11 | Key? key, 12 | required this.title, 13 | required this.labelText, 14 | this.initialValue, 15 | this.confirmButtonText = 'Confirm', 16 | this.validator, 17 | }) : super(key: key); 18 | 19 | @override 20 | State createState() => _TextInputDialogState(); 21 | } 22 | 23 | class _TextInputDialogState extends State { 24 | late TextEditingController _controller; 25 | final _formKey = GlobalKey(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _controller = TextEditingController(text: widget.initialValue); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _controller.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | void _submit() { 40 | if (_formKey.currentState!.validate()) { 41 | Navigator.of(context).pop(_controller.text.trim()); 42 | } 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return AlertDialog( 48 | title: Text(widget.title), 49 | content: Form( 50 | key: _formKey, 51 | child: TextFormField( 52 | controller: _controller, 53 | autofocus: true, 54 | decoration: InputDecoration( 55 | labelText: widget.labelText, 56 | border: const OutlineInputBorder(), 57 | ), 58 | validator: widget.validator, 59 | onFieldSubmitted: (_) => _submit(), 60 | ), 61 | ), 62 | actions: [ 63 | TextButton( 64 | onPressed: () => Navigator.of(context).pop(), // Return null on cancel 65 | child: const Text('Cancel'), 66 | ), 67 | FilledButton( 68 | onPressed: _submit, 69 | child: Text(widget.confirmButtonText), 70 | ), 71 | ], 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /lib/widgets/window_buttons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:window_manager/window_manager.dart'; 3 | 4 | class WindowButtons extends StatelessWidget { 5 | const WindowButtons({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Row( 10 | children: [ 11 | _WindowButton( 12 | icon: Icons.remove, 13 | onPressed: () => windowManager.minimize(), 14 | tooltip: 'Minimize', 15 | ), 16 | _WindowButton( 17 | icon: Icons.crop_square, 18 | onPressed: () async { 19 | if (await windowManager.isMaximized()) { 20 | windowManager.unmaximize(); 21 | } else { 22 | windowManager.maximize(); 23 | } 24 | }, 25 | tooltip: 'Maximize', 26 | ), 27 | _WindowButton( 28 | icon: Icons.close, 29 | onPressed: () => windowManager.close(), 30 | tooltip: 'Close', 31 | isClose: true, 32 | ), 33 | ], 34 | ); 35 | } 36 | } 37 | 38 | class _WindowButton extends StatelessWidget { 39 | final IconData icon; 40 | final VoidCallback onPressed; 41 | final String tooltip; 42 | final bool isClose; 43 | 44 | const _WindowButton({ 45 | required this.icon, 46 | required this.onPressed, 47 | required this.tooltip, 48 | this.isClose = false, 49 | }); 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return SizedBox( 54 | width: 46, 55 | height: 32, 56 | child: Tooltip( 57 | message: tooltip, 58 | child: Material( 59 | color: Colors.transparent, 60 | child: InkWell( 61 | onTap: onPressed, 62 | hoverColor: isClose ? Colors.red : Colors.grey.withOpacity(0.2), 63 | child: Icon( 64 | icon, 65 | size: 16, 66 | color: Theme.of(context).iconTheme.color, 67 | ), 68 | ), 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/widgets/wine_component_settings_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import '../models/settings.dart'; 4 | import '../providers/settings_provider.dart'; 5 | import '../services/log_service.dart'; 6 | 7 | class WineComponentSettingsDialog extends StatefulWidget { 8 | const WineComponentSettingsDialog({Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _WineComponentSettingsDialogState(); 12 | } 13 | 14 | class _WineComponentSettingsDialogState extends State { 15 | final _formKey = GlobalKey(); 16 | Settings? _settings; 17 | bool _isLoading = true; 18 | 19 | // Controllers for URL settings 20 | late TextEditingController _dxvkApiUrlController; 21 | late TextEditingController _vkd3dApiUrlController; 22 | late TextEditingController _wineBuildsApiUrlController; 23 | late TextEditingController _protonGeApiUrlController; 24 | late TextEditingController _kronekProtonApiUrlController; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | // Initialize controllers 30 | _dxvkApiUrlController = TextEditingController(); 31 | _vkd3dApiUrlController = TextEditingController(); 32 | _wineBuildsApiUrlController = TextEditingController(); 33 | _protonGeApiUrlController = TextEditingController(); 34 | _kronekProtonApiUrlController = TextEditingController(); 35 | _loadSettings(); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | // Dispose controllers 41 | _dxvkApiUrlController.dispose(); 42 | _vkd3dApiUrlController.dispose(); 43 | _wineBuildsApiUrlController.dispose(); 44 | _protonGeApiUrlController.dispose(); 45 | _kronekProtonApiUrlController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | Future _loadSettings() async { 50 | if (!mounted) return; 51 | setState(() { 52 | _isLoading = true; 53 | }); 54 | 55 | final settingsProvider = Provider.of(context, listen: false); 56 | final currentSettings = settingsProvider.settings; 57 | 58 | if (!mounted) return; 59 | setState(() { 60 | _settings = currentSettings; 61 | _dxvkApiUrlController.text = currentSettings.dxvkApiUrl; 62 | _vkd3dApiUrlController.text = currentSettings.vkd3dApiUrl; 63 | _wineBuildsApiUrlController.text = currentSettings.wineBuildsApiUrl; 64 | _protonGeApiUrlController.text = currentSettings.protonGeApiUrl; 65 | _kronekProtonApiUrlController.text = currentSettings.kronekProtonApiUrl; 66 | _isLoading = false; 67 | }); 68 | } 69 | 70 | Future _saveSettings() async { 71 | if (_formKey.currentState!.validate()) { 72 | if (!mounted) return; 73 | setState(() { 74 | _isLoading = true; 75 | }); 76 | 77 | final settingsProvider = Provider.of(context, listen: false); 78 | final currentSettings = settingsProvider.settings; 79 | final logService = Provider.of(context, listen: false); 80 | 81 | final newSettings = currentSettings.copyWith( 82 | dxvkApiUrl: _dxvkApiUrlController.text.trim(), 83 | vkd3dApiUrl: _vkd3dApiUrlController.text.trim(), 84 | wineBuildsApiUrl: _wineBuildsApiUrlController.text.trim(), 85 | protonGeApiUrl: _protonGeApiUrlController.text.trim(), 86 | kronekProtonApiUrl: _kronekProtonApiUrlController.text.trim(), 87 | ); 88 | 89 | try { 90 | await settingsProvider.updateSettings(newSettings); 91 | logService.log('Wine component settings updated successfully.'); 92 | if (mounted) { 93 | ScaffoldMessenger.of(context).showSnackBar( 94 | const SnackBar(content: Text('Wine component settings saved!')), 95 | ); 96 | Navigator.of(context).pop(); // Close dialog on success 97 | } 98 | } catch (e) { 99 | logService.log('Failed to save Wine component settings: $e', LogLevel.error); 100 | if (mounted) { 101 | ScaffoldMessenger.of(context).showSnackBar( 102 | SnackBar(content: Text('Error saving settings: $e'), backgroundColor: Colors.red), 103 | ); 104 | } 105 | } finally { 106 | if (mounted) { 107 | setState(() { 108 | _isLoading = false; 109 | }); 110 | } 111 | } 112 | } 113 | } 114 | 115 | Widget _buildUrlSettingField({ 116 | required TextEditingController controller, 117 | required String label, 118 | required String hint, 119 | IconData? icon, 120 | }) { 121 | return Padding( 122 | padding: const EdgeInsets.symmetric(vertical: 8.0), 123 | child: TextFormField( 124 | controller: controller, 125 | decoration: InputDecoration( 126 | labelText: label, 127 | hintText: hint, 128 | border: OutlineInputBorder( 129 | borderRadius: BorderRadius.circular(8.0), 130 | ), 131 | prefixIcon: icon != null ? Icon(icon) : null, 132 | ), 133 | validator: (value) { 134 | if (value == null || value.trim().isEmpty) { 135 | return 'URL cannot be empty'; 136 | } 137 | if (!(Uri.tryParse(value.trim())?.isAbsolute == true)) { 138 | return 'Please enter a valid URL'; 139 | } 140 | return null; 141 | }, 142 | ), 143 | ); 144 | } 145 | 146 | @override 147 | Widget build(BuildContext context) { 148 | if (_isLoading) { 149 | return const Dialog( 150 | child: Padding( 151 | padding: EdgeInsets.all(20.0), 152 | child: Column( 153 | mainAxisSize: MainAxisSize.min, 154 | children: [ 155 | CircularProgressIndicator(), 156 | SizedBox(height: 20), 157 | Text('Loading settings...'), 158 | ], 159 | ), 160 | ), 161 | ); 162 | } 163 | 164 | return AlertDialog( 165 | title: const Text('Wine Component Settings'), 166 | content: SingleChildScrollView( 167 | child: Form( 168 | key: _formKey, 169 | child: Column( 170 | mainAxisSize: MainAxisSize.min, 171 | children: [ 172 | _buildUrlSettingField( 173 | controller: _dxvkApiUrlController, 174 | label: 'DXVK Releases API URL', 175 | hint: 'e.g., https://api.github.com/repos/doitsujin/dxvk/releases', 176 | icon: Icons.code, 177 | ), 178 | _buildUrlSettingField( 179 | controller: _vkd3dApiUrlController, 180 | label: 'VKD3D-Proton Releases API URL', 181 | hint: 'e.g., https://api.github.com/repos/HansKristian-Work/vkd3d-proton/releases', 182 | icon: Icons.code_off, 183 | ), 184 | _buildUrlSettingField( 185 | controller: _wineBuildsApiUrlController, 186 | label: 'Kronek Wine Builds API URL', 187 | hint: 'e.g., https://api.github.com/repos/Kron4ek/Wine-Builds/releases', 188 | icon: Icons.wine_bar, 189 | ), 190 | _buildUrlSettingField( 191 | controller: _protonGeApiUrlController, 192 | label: 'Proton-GE Releases API URL', 193 | hint: 'e.g., https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases', 194 | icon: Icons.gamepad, 195 | ), 196 | _buildUrlSettingField( 197 | controller: _kronekProtonApiUrlController, 198 | label: 'Kronek Proton Builds API URL', 199 | hint: 'e.g., https://api.github.com/repos/Kron4ek/Proton-Wine-Builds/releases', 200 | icon: Icons.rocket_launch, 201 | ), 202 | ], 203 | ), 204 | ), 205 | ), 206 | actions: [ 207 | TextButton( 208 | onPressed: () => Navigator.of(context).pop(), 209 | child: const Text('Cancel'), 210 | ), 211 | ElevatedButton( 212 | onPressed: _isLoading ? null : _saveSettings, 213 | child: _isLoading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Save'), 214 | ), 215 | ], 216 | ); 217 | } 218 | } -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.13) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "wine_prefix_manager") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.example.wine_prefix_manager") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | # Application build; see runner/CMakeLists.txt. 58 | add_subdirectory("runner") 59 | 60 | # Run the Flutter tool portions of the build. This must not be removed. 61 | add_dependencies(${BINARY_NAME} flutter_assemble) 62 | 63 | # Only the install-generated bundle's copy of the executable will launch 64 | # correctly, since the resources must in the right relative locations. To avoid 65 | # people trying to run the unbundled copy, put it in a subdirectory instead of 66 | # the default top-level location. 67 | set_target_properties(${BINARY_NAME} 68 | PROPERTIES 69 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 70 | ) 71 | 72 | 73 | # Generated plugin build rules, which manage building the plugins and adding 74 | # them to the application. 75 | include(flutter/generated_plugins.cmake) 76 | 77 | 78 | # === Installation === 79 | # By default, "installing" just makes a relocatable bundle in the build 80 | # directory. 81 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 82 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 83 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 84 | endif() 85 | 86 | # Start with a clean build bundle directory every time. 87 | install(CODE " 88 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 89 | " COMPONENT Runtime) 90 | 91 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 92 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 93 | 94 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 95 | COMPONENT Runtime) 96 | 97 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 98 | COMPONENT Runtime) 99 | 100 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 101 | COMPONENT Runtime) 102 | 103 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 104 | install(FILES "${bundled_library}" 105 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 106 | COMPONENT Runtime) 107 | endforeach(bundled_library) 108 | 109 | # Copy the native assets provided by the build.dart from all packages. 110 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 111 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 112 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 113 | COMPONENT Runtime) 114 | 115 | # Fully re-copy the assets directory on each build to avoid having stale files 116 | # from a previous install. 117 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 118 | install(CODE " 119 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 120 | " COMPONENT Runtime) 121 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 122 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 123 | 124 | # Install the AOT library on non-Debug builds only. 125 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 126 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 127 | COMPONENT Runtime) 128 | endif() 129 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | void fl_register_plugins(FlPluginRegistry* registry) { 15 | g_autoptr(FlPluginRegistrar) file_saver_registrar = 16 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); 17 | file_saver_plugin_register_with_registrar(file_saver_registrar); 18 | g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); 20 | screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); 21 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 23 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 24 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 26 | window_manager_plugin_register_with_registrar(window_manager_registrar); 27 | } 28 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_saver 7 | screen_retriever_linux 8 | url_launcher_linux 9 | window_manager 10 | ) 11 | 12 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 13 | ) 14 | 15 | set(PLUGIN_BUNDLED_LIBRARIES) 16 | 17 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 18 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 19 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 22 | endforeach(plugin) 23 | 24 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 25 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 27 | endforeach(ffi_plugin) 28 | -------------------------------------------------------------------------------- /linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use X11 rendering directly when possible 24 | GdkScreen* screen = gtk_window_get_screen(window); 25 | if (GDK_IS_X11_SCREEN(screen)) { 26 | gtk_window_set_default_visual(window, gdk_screen_get_system_visual(screen)); 27 | } 28 | 29 | // Set up window 30 | gtk_window_set_title(window, "Wine Prefix Manager"); 31 | gtk_window_set_default_size(window, 1000, 700); 32 | 33 | // Force software rendering mode for GTK visuals 34 | const char* software_gl = g_getenv("LIBGL_ALWAYS_SOFTWARE"); 35 | if (software_gl == nullptr) { 36 | g_setenv("LIBGL_ALWAYS_SOFTWARE", "1", TRUE); 37 | } 38 | 39 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 40 | FlView* view = fl_view_new(project); 41 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 42 | 43 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 44 | 45 | gtk_widget_show_all(GTK_WIDGET(window)); 46 | gtk_widget_grab_focus(GTK_WIDGET(view)); 47 | } 48 | 49 | // Implements GApplication::open. 50 | static void my_application_open(GApplication* application, 51 | GFile** files, 52 | gint n_files, 53 | const gchar* hint) { 54 | MyApplication* self = MY_APPLICATION(application); 55 | // Handle file opening logic here 56 | } 57 | 58 | // Implements GObject::dispose. 59 | static void my_application_dispose(GObject* object) { 60 | MyApplication* self = MY_APPLICATION(object); 61 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 62 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 63 | } 64 | 65 | // Implements GObject::class_init. 66 | static void my_application_class_init(MyApplicationClass* klass) { 67 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 68 | G_APPLICATION_CLASS(klass)->open = my_application_open; 69 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 70 | } 71 | 72 | // Implements GObject::init. 73 | static void my_application_init(MyApplication* self) {} 74 | 75 | // Creates a new instance of MyApplication. 76 | MyApplication* my_application_new() { 77 | return MY_APPLICATION(g_object_new(my_application_get_type(), 78 | "application-id", "com.example.wine_prefix_manager", 79 | "flags", G_APPLICATION_NON_UNIQUE, 80 | nullptr)); 81 | } -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/runner/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "wine_prefix_manager"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "wine_prefix_manager"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GApplication::startup. 85 | static void my_application_startup(GApplication* application) { 86 | //MyApplication* self = MY_APPLICATION(object); 87 | 88 | // Perform any actions required at application startup. 89 | 90 | G_APPLICATION_CLASS(my_application_parent_class)->startup(application); 91 | } 92 | 93 | // Implements GApplication::shutdown. 94 | static void my_application_shutdown(GApplication* application) { 95 | //MyApplication* self = MY_APPLICATION(object); 96 | 97 | // Perform any actions required at application shutdown. 98 | 99 | G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); 100 | } 101 | 102 | // Implements GObject::dispose. 103 | static void my_application_dispose(GObject* object) { 104 | MyApplication* self = MY_APPLICATION(object); 105 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 106 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 107 | } 108 | 109 | static void my_application_class_init(MyApplicationClass* klass) { 110 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 111 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 112 | G_APPLICATION_CLASS(klass)->startup = my_application_startup; 113 | G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; 114 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 115 | } 116 | 117 | static void my_application_init(MyApplication* self) {} 118 | 119 | MyApplication* my_application_new() { 120 | // Set the program name to the application ID, which helps various systems 121 | // like GTK and desktop environments map this running application to its 122 | // corresponding .desktop file. This ensures better integration by allowing 123 | // the application to be recognized beyond its binary name. 124 | g_set_prgname(APPLICATION_ID); 125 | 126 | return MY_APPLICATION(g_object_new(my_application_get_type(), 127 | "application-id", APPLICATION_ID, 128 | "flags", G_APPLICATION_NON_UNIQUE, 129 | nullptr)); 130 | } 131 | -------------------------------------------------------------------------------- /linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: wine_prefix_manager 2 | description: A wine prefix manager for Linux gaming 3 | publish_to: 'none' 4 | version: 3.3.1+1748701400 5 | 6 | environment: 7 | sdk: '>=3.0.0 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | # Cross-platform and Linux dependencies only 13 | shared_preferences: ^2.2.1 14 | provider: ^6.0.0 15 | path: ^1.8.0 16 | path_provider: ^2.0.0 # Added for logs page 17 | http: ^1.1.0 # For API requests to GitHub 18 | dio: ^5.0.0 19 | process_run: ^0.13.0 20 | file_picker: ^6.0.0 21 | window_manager: ^0.4.3 # Updated version 22 | archive: ^3.3.7 # For tar.gz extraction 23 | collection: ^1.17.0 # Added for groupBy 24 | package_info_plus: ^5.0.1 # Added for version info 25 | # Use a specific version of bitsdojo_window with Linux fixes # Removed unused bitsdojo_window 26 | # bitsdojo_window: ^0.1.5 27 | intl: ^0.18.1 28 | share_plus: ^7.0.2 29 | file_saver: ^0.2.4 30 | url_launcher: ^6.1.14 31 | flutter_dotenv: ^5.1.0 # For environment variable management 32 | # ...any other dependencies you need... 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | flutter_lints: ^2.0.0 38 | 39 | flutter: 40 | uses-material-design: true 41 | assets: 42 | - .env 43 | -------------------------------------------------------------------------------- /scripts/build_appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple AppImage builder for Wine Prefix Manager 4 | 5 | set -e 6 | 7 | # Colors 8 | GREEN='\033[0;32m' 9 | BLUE='\033[0;34m' 10 | RED='\033[0;31m' 11 | NC='\033[0m' 12 | 13 | # Get project info 14 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 15 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 16 | VERSION=$(grep "^version:" "$PROJECT_ROOT/pubspec.yaml" | sed 's/version: *//g' | sed 's/+.*//' | tr -d '"' | tr -d "'") 17 | 18 | echo -e "${BLUE}Building AppImage for Wine Prefix Manager v$VERSION${NC}" 19 | 20 | # Build Flutter app 21 | echo -e "${BLUE}Building Flutter app...${NC}" 22 | cd "$PROJECT_ROOT" 23 | flutter clean 24 | flutter pub get 25 | flutter build linux --release 26 | 27 | BUILD_DIR="$PROJECT_ROOT/build/linux/x64/release/bundle" 28 | APPIMAGE_DIR="$PROJECT_ROOT/appimage" 29 | APPDIR="$APPIMAGE_DIR/WinePrefixManager.AppDir" 30 | 31 | if [ ! -d "$BUILD_DIR" ]; then 32 | echo -e "${RED}Flutter build failed${NC}" 33 | exit 1 34 | fi 35 | 36 | # Create AppDir 37 | echo -e "${BLUE}Creating AppDir...${NC}" 38 | rm -rf "$APPDIR" 39 | mkdir -p "$APPDIR" 40 | 41 | # Copy Flutter app 42 | cp -r "$BUILD_DIR"/* "$APPDIR/" 43 | chmod +x "$APPDIR/wine_prefix_manager" 44 | 45 | # Create desktop file 46 | cat > "$APPDIR/wine_prefix_manager.desktop" << EOF 47 | [Desktop Entry] 48 | Type=Application 49 | Name=Wine Prefix Manager 50 | Comment=Manage Wine prefixes with ease 51 | Exec=wine_prefix_manager 52 | Icon=wine_prefix_manager 53 | Categories=System;Utility; 54 | Terminal=false 55 | EOF 56 | 57 | # Create simple icon (if not exists) 58 | if [ ! -f "$APPDIR/wine_prefix_manager.png" ]; then 59 | # Create a simple colored square as fallback icon 60 | echo -e "${BLUE}Creating fallback icon...${NC}" 61 | cat > "$APPDIR/wine_prefix_manager.svg" << 'EOF' 62 | 63 | 64 | 65 | W 66 | 67 | EOF 68 | 69 | if command -v convert &> /dev/null; then 70 | convert "$APPDIR/wine_prefix_manager.svg" "$APPDIR/wine_prefix_manager.png" 71 | rm "$APPDIR/wine_prefix_manager.svg" 72 | else 73 | mv "$APPDIR/wine_prefix_manager.svg" "$APPDIR/wine_prefix_manager.png" 74 | fi 75 | fi 76 | 77 | # Create AppRun 78 | cat > "$APPDIR/AppRun" << 'EOF' 79 | #!/bin/bash 80 | APPDIR="$(dirname "$(readlink -f "$0")")" 81 | export PATH="$APPDIR:$PATH" 82 | exec "$APPDIR/wine_prefix_manager" "$@" 83 | EOF 84 | chmod +x "$APPDIR/AppRun" 85 | 86 | # Download appimagetool if needed 87 | cd "$APPIMAGE_DIR" 88 | if [ ! -f "appimagetool-x86_64.AppImage" ]; then 89 | echo -e "${BLUE}Downloading appimagetool...${NC}" 90 | wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" 91 | chmod +x appimagetool-x86_64.AppImage 92 | fi 93 | 94 | # Build AppImage 95 | echo -e "${BLUE}Building AppImage...${NC}" 96 | OUTPUT_NAME="WinePrefixManager-$VERSION-x86_64.AppImage" 97 | ./appimagetool-x86_64.AppImage "$APPDIR" "$OUTPUT_NAME" --no-appstream 98 | 99 | if [ -f "$OUTPUT_NAME" ]; then 100 | echo -e "${GREEN}✅ AppImage created: $OUTPUT_NAME${NC}" 101 | echo -e "${GREEN}Location: $APPIMAGE_DIR/$OUTPUT_NAME${NC}" 102 | else 103 | echo -e "${RED}❌ AppImage creation failed${NC}" 104 | exit 1 105 | fi -------------------------------------------------------------------------------- /scripts/check_version_sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple version sync checker for Wine Prefix Manager 4 | 5 | set -e 6 | 7 | # Colors 8 | GREEN='\033[0;32m' 9 | BLUE='\033[0;34m' 10 | RED='\033[0;31m' 11 | YELLOW='\033[1;33m' 12 | NC='\033[0m' 13 | 14 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 15 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 16 | PUBSPEC_FILE="$PROJECT_ROOT/pubspec.yaml" 17 | 18 | # Parse arguments 19 | FIX_MODE=false 20 | if [[ "$1" == "--fix" ]]; then 21 | FIX_MODE=true 22 | fi 23 | 24 | if [[ "$1" == "--help" || "$1" == "-h" ]]; then 25 | echo "Version Sync Checker" 26 | echo "" 27 | echo "Usage: $0 [--fix]" 28 | echo " --fix Create git tag to match pubspec.yaml version" 29 | echo " --help Show this help" 30 | exit 0 31 | fi 32 | 33 | # Get version from pubspec.yaml 34 | get_pubspec_version() { 35 | if [[ ! -f "$PUBSPEC_FILE" ]]; then 36 | echo -e "${RED}Error: pubspec.yaml not found${NC}" 37 | exit 2 38 | fi 39 | 40 | grep "^version:" "$PUBSPEC_FILE" | sed 's/version: *//g' | sed 's/+.*//' | tr -d '"' | tr -d "'" 41 | } 42 | 43 | # Get latest git tag 44 | get_latest_git_tag() { 45 | if ! command -v git &> /dev/null; then 46 | echo -e "${RED}Error: git not found${NC}" 47 | exit 2 48 | fi 49 | 50 | if ! git rev-parse --git-dir > /dev/null 2>&1; then 51 | echo -e "${YELLOW}Warning: Not in a git repository${NC}" 52 | return 1 53 | fi 54 | 55 | git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || return 1 56 | } 57 | 58 | echo -e "${BLUE}Checking version sync...${NC}" 59 | 60 | # Get versions 61 | PUBSPEC_VERSION=$(get_pubspec_version) 62 | echo -e "Pubspec version: ${YELLOW}$PUBSPEC_VERSION${NC}" 63 | 64 | if GIT_VERSION=$(get_latest_git_tag); then 65 | echo -e "Git tag version: ${YELLOW}$GIT_VERSION${NC}" 66 | 67 | if [[ "$PUBSPEC_VERSION" == "$GIT_VERSION" ]]; then 68 | echo -e "${GREEN}✅ Versions are in sync!${NC}" 69 | exit 0 70 | else 71 | echo -e "${RED}❌ Versions are out of sync!${NC}" 72 | 73 | if [[ "$FIX_MODE" == true ]]; then 74 | echo -e "${BLUE}Creating git tag v$PUBSPEC_VERSION...${NC}" 75 | if git tag -a "v$PUBSPEC_VERSION" -m "Release version $PUBSPEC_VERSION"; then 76 | echo -e "${GREEN}✅ Git tag created${NC}" 77 | echo -e "${YELLOW}Push with: git push origin v$PUBSPEC_VERSION${NC}" 78 | else 79 | echo -e "${RED}❌ Failed to create tag${NC}" 80 | exit 2 81 | fi 82 | else 83 | echo -e "${YELLOW}Fix with: $0 --fix${NC}" 84 | exit 1 85 | fi 86 | fi 87 | else 88 | echo -e "Git tag version: ${YELLOW}none${NC}" 89 | 90 | if [[ "$FIX_MODE" == true ]]; then 91 | echo -e "${BLUE}Creating first git tag v$PUBSPEC_VERSION...${NC}" 92 | if git tag -a "v$PUBSPEC_VERSION" -m "Release version $PUBSPEC_VERSION"; then 93 | echo -e "${GREEN}✅ Git tag created${NC}" 94 | echo -e "${YELLOW}Push with: git push origin v$PUBSPEC_VERSION${NC}" 95 | else 96 | echo -e "${RED}❌ Failed to create tag${NC}" 97 | exit 2 98 | fi 99 | else 100 | echo -e "${YELLOW}Create tag with: git tag -a v$PUBSPEC_VERSION -m \"Release version $PUBSPEC_VERSION\"${NC}" 101 | echo -e "${YELLOW}Or fix with: $0 --fix${NC}" 102 | exit 1 103 | fi 104 | fi -------------------------------------------------------------------------------- /scripts/create_github_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple GitHub Release Creator for Wine Prefix Manager 4 | 5 | set -e 6 | 7 | # Colors 8 | GREEN='\033[0;32m' 9 | BLUE='\033[0;34m' 10 | RED='\033[0;31m' 11 | YELLOW='\033[1;33m' 12 | NC='\033[0m' 13 | 14 | # Configuration 15 | REPO_OWNER="CrownParkComputing" 16 | REPO_NAME="wine_prefix_manager" 17 | 18 | # Parse arguments 19 | if [[ "$1" == "--version" ]]; then 20 | VERSION="$2" 21 | else 22 | echo -e "${RED}Usage: $0 --version VERSION${NC}" 23 | echo "Example: $0 --version 3.3.1" 24 | exit 1 25 | fi 26 | 27 | if [[ -z "$VERSION" ]]; then 28 | echo -e "${RED}Version required${NC}" 29 | exit 1 30 | fi 31 | 32 | # Check for GitHub token 33 | if [[ -z "$GITHUB_TOKEN" ]]; then 34 | echo -e "${RED}GITHUB_TOKEN environment variable required${NC}" 35 | echo "Get a token from: https://github.com/settings/tokens" 36 | exit 1 37 | fi 38 | 39 | PROJECT_ROOT="$(dirname "$(dirname "$(realpath "$0")")")" 40 | APPIMAGE_FILE="$PROJECT_ROOT/appimage/WinePrefixManager-$VERSION-x86_64.AppImage" 41 | 42 | # Check if AppImage exists 43 | if [[ ! -f "$APPIMAGE_FILE" ]]; then 44 | echo -e "${RED}AppImage not found: $APPIMAGE_FILE${NC}" 45 | echo "Build it first with: make appimage" 46 | exit 1 47 | fi 48 | 49 | # Check if git tag exists 50 | if ! git rev-parse "v$VERSION" >/dev/null 2>&1; then 51 | echo -e "${RED}Git tag v$VERSION does not exist${NC}" 52 | echo "Create it first with: git tag -a v$VERSION -m \"Release v$VERSION\"" 53 | exit 1 54 | fi 55 | 56 | echo -e "${BLUE}Creating GitHub release v$VERSION...${NC}" 57 | 58 | # Create simple release notes 59 | RELEASE_NOTES="Release v$VERSION 60 | 61 | Download the AppImage below and run: 62 | \`\`\`bash 63 | chmod +x WinePrefixManager-$VERSION-x86_64.AppImage 64 | ./WinePrefixManager-$VERSION-x86_64.AppImage 65 | \`\`\` 66 | 67 | ## Changes 68 | $(git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 v$VERSION^ 2>/dev/null || git rev-list --max-parents=0 HEAD)..v$VERSION || echo "- Initial release") 69 | 70 | ## System Requirements 71 | - Linux x86_64 72 | - Wine (for running Windows applications) 73 | - GTK 3.0+ (usually pre-installed)" 74 | 75 | # Create release using GitHub API 76 | echo -e "${BLUE}Creating release on GitHub...${NC}" 77 | RELEASE_RESPONSE=$(curl -s -X POST \ 78 | -H "Authorization: token $GITHUB_TOKEN" \ 79 | -H "Accept: application/vnd.github.v3+json" \ 80 | "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases" \ 81 | -d "{ 82 | \"tag_name\": \"v$VERSION\", 83 | \"name\": \"Wine Prefix Manager v$VERSION\", 84 | \"body\": $(echo "$RELEASE_NOTES" | jq -R -s .), 85 | \"draft\": false, 86 | \"prerelease\": false 87 | }") 88 | 89 | # Extract upload URL 90 | UPLOAD_URL=$(echo "$RELEASE_RESPONSE" | grep -o '"upload_url": "[^"]*' | sed 's/"upload_url": "//' | sed 's/{?name,label}//') 91 | 92 | if [[ -z "$UPLOAD_URL" ]]; then 93 | echo -e "${RED}Failed to create release${NC}" 94 | echo "$RELEASE_RESPONSE" 95 | exit 1 96 | fi 97 | 98 | echo -e "${GREEN}✅ Release created${NC}" 99 | 100 | # Upload AppImage 101 | echo -e "${BLUE}Uploading AppImage...${NC}" 102 | APPIMAGE_NAME="WinePrefixManager-$VERSION-x86_64.AppImage" 103 | 104 | curl -s -X POST \ 105 | -H "Authorization: token $GITHUB_TOKEN" \ 106 | -H "Content-Type: application/octet-stream" \ 107 | --data-binary @"$APPIMAGE_FILE" \ 108 | "$UPLOAD_URL?name=$APPIMAGE_NAME&label=$APPIMAGE_NAME" 109 | 110 | echo -e "${GREEN}✅ AppImage uploaded${NC}" 111 | echo -e "${GREEN}🎉 Release v$VERSION created successfully!${NC}" 112 | echo -e "${BLUE}View at: https://github.com/$REPO_OWNER/$REPO_NAME/releases/tag/v$VERSION${NC}" -------------------------------------------------------------------------------- /scripts/setup_iso_mounting.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup script for Wine Prefix Manager ISO mounting 4 | # This script configures sudo permissions for mounting operations 5 | 6 | echo "Wine Prefix Manager - ISO Mounting Setup" 7 | echo "=======================================" 8 | echo "" 9 | echo "This script will configure sudo permissions to allow ISO mounting" 10 | echo "without password prompts for the following commands:" 11 | echo " - losetup (for creating loop devices)" 12 | echo " - mount (for mounting ISOs)" 13 | echo " - umount (for unmounting ISOs)" 14 | echo "" 15 | 16 | # Check if user has sudo privileges 17 | if ! sudo -n true 2>/dev/null; then 18 | echo "❌ You need sudo privileges to run this setup." 19 | echo "Please run: sudo $0" 20 | exit 1 21 | fi 22 | 23 | # Get the actual username (in case script is run with sudo) 24 | if [ -n "$SUDO_USER" ]; then 25 | USERNAME="$SUDO_USER" 26 | else 27 | USERNAME="$USER" 28 | fi 29 | 30 | echo "Setting up permissions for user: $USERNAME" 31 | echo "" 32 | 33 | # Create sudoers rule 34 | SUDOERS_RULE="$USERNAME ALL=(ALL) NOPASSWD: /usr/bin/losetup, /usr/bin/mount, /usr/bin/umount" 35 | SUDOERS_FILE="/etc/sudoers.d/wine-prefix-manager-iso" 36 | 37 | # Write the rule to a sudoers file 38 | echo "$SUDOERS_RULE" | sudo tee "$SUDOERS_FILE" > /dev/null 39 | 40 | # Set proper permissions 41 | sudo chmod 440 "$SUDOERS_FILE" 42 | 43 | # Validate the sudoers file 44 | if sudo visudo -c -f "$SUDOERS_FILE" > /dev/null 2>&1; then 45 | echo "✅ Successfully configured sudo permissions!" 46 | echo "" 47 | echo "The following file has been created:" 48 | echo " $SUDOERS_FILE" 49 | echo "" 50 | echo "You can now use ISO mounting in Wine Prefix Manager without" 51 | echo "password prompts for mounting operations." 52 | echo "" 53 | echo "To remove these permissions later, run:" 54 | echo " sudo rm $SUDOERS_FILE" 55 | else 56 | echo "❌ Error: Failed to create valid sudoers configuration." 57 | sudo rm -f "$SUDOERS_FILE" 58 | exit 1 59 | fi 60 | 61 | echo "" 62 | echo "Setup complete! You can now restart Wine Prefix Manager and try" 63 | echo "mounting ISOs without password prompts." -------------------------------------------------------------------------------- /website/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debug Download 7 | 8 | 9 |

Debug Download Functionality

10 | 11 | 12 |
13 | 14 | 50 | 51 | -------------------------------------------------------------------------------- /website/images/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/about.png -------------------------------------------------------------------------------- /website/images/backup-creator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/backup-creator.png -------------------------------------------------------------------------------- /website/images/backup-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/backup-manager.png -------------------------------------------------------------------------------- /website/images/gamelibrary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/gamelibrary.png -------------------------------------------------------------------------------- /website/images/hero-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/hero-screenshot.png -------------------------------------------------------------------------------- /website/images/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/logs.png -------------------------------------------------------------------------------- /website/images/prefixcreator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/prefixcreator.png -------------------------------------------------------------------------------- /website/images/prefixmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/prefixmanager.png -------------------------------------------------------------------------------- /website/images/protonprefixes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrownParkComputing/wine_prefix_manager/59e29f1f58ae24ce24280ac9097b14cf7e8139ee/website/images/protonprefixes.png -------------------------------------------------------------------------------- /website/serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple local server for testing the website 4 | echo "🚀 Starting Wine Prefix Manager Website locally..." 5 | echo "📍 URL: http://localhost:8000" 6 | echo "🛑 Press Ctrl+C to stop" 7 | echo "" 8 | 9 | # Check if Python 3 is available 10 | if command -v python3 &> /dev/null; then 11 | python3 -m http.server 8000 12 | elif command -v python &> /dev/null; then 13 | python -m http.server 8000 14 | else 15 | echo "❌ Error: Python is required to serve the website locally" 16 | echo "Please install Python 3 or use a different web server" 17 | exit 1 18 | fi -------------------------------------------------------------------------------- /wine_prefix_manager.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Wine Prefix Manager 4 | Comment=Manage Wine prefixes on Linux 5 | Exec=/home/jon/Desktop/wine_prefix_manager/build/linux/x64/release/bundle/wine_prefix_manager 6 | Icon=/home/jon/Desktop/wine_prefix_manager/build/linux/x64/release/bundle/data/flutter_assets/assets/icon.png 7 | Terminal=false 8 | Categories=Utility;Development; 9 | --------------------------------------------------------------------------------