[
  {
    "path": ".claude/memory/feedback_coding_boundaries.md",
    "content": "---\nname: coding_boundaries\ndescription: User wants to write all non-trivial logic themselves — Claude should only review, suggest, and handle boilerplate\ntype: feedback\n---\n\nNever write the \"hard parts\" — architecture decisions, core business logic, state management patterns, bug fix implementations, algorithm design. Instead, review the user's code, point out issues, suggest approaches, and explain tradeoffs. Let the user implement it.\n\n**Why:** The user noticed their coding instincts and skills declining from over-delegating to Claude. They want to stay sharp by doing the thinking and implementation themselves.\n\n**How to apply:**\n- **Hard parts** (user codes): ViewModel logic, repository implementations, state flows, bug fixes, architectural patterns, cache strategies, concurrency handling, UI interaction logic. For these — review, suggest, explain, but don't write the code.\n- **Boilerplate** (Claude codes): repetitive refactors, string resources, migration scaffolding, import fixes, build config, copy-paste patterns, test scaffolding, file moves/renames.\n- When the user asks to fix a bug or implement a feature, describe what's wrong and suggest an approach — then let them write it.\n- If the user explicitly asks \"just do it\" for something non-trivial, remind them of this agreement first.\n"
  },
  {
    "path": ".coderabbit.yaml",
    "content": "language: \"en-US\"\nearly_access: false\nreviews:\n  profile: \"chill\"\n  request_changes_workflow: false\n  high_level_summary: true\n  poem: true\n  review_status: true\n  collapse_walkthrough: false\n  auto_review:\n    enabled: true\n    drafts: false\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*.{kt,kts}]\nktlint_standard_filename = disabled\nktlint_standard_no-wildcard-imports = disabled\nktlint_function_naming_ignore_when_annotated_with = Composable"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [rainxchzed]\nbuy_me_a_coffee: rainxchzed\ncustom: https://golden-kodee.awardsplatform.com/entry/vote/mNKjQxkX/vnZAamgg?search=8154c88ed0eccba9-70\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.md",
    "content": "---\nname: Custom issue template\nabout: Describe this issue template's purpose here.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/build-desktop-platforms.yml",
    "content": "name: Build Desktop Platform Installers\n\non:\n  push:\n    branches:\n      - generate-installers\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  JAVA_VERSION: '21'\n  JAVA_DISTRIBUTION: 'temurin'\n  GRADLE_OPTS: >-\n    -Dorg.gradle.daemon=false\n    -Dorg.gradle.parallel=true\n    -Dorg.gradle.caching=true\n    -Dorg.gradle.vfs.watch=false\n\njobs:\n  build-windows:\n    runs-on: windows-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Validate Gradle wrapper\n        uses: gradle/actions/wrapper-validation@v4\n\n      - name: Set up JDK ${{ env.JAVA_VERSION }}\n        uses: actions/setup-java@v4\n        with:\n          distribution: ${{ env.JAVA_DISTRIBUTION }}\n          java-version: ${{ env.JAVA_VERSION }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v4\n        with:\n          cache-read-only: false\n          cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}\n          gradle-home-cache-cleanup: true\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n        shell: bash\n\n      - name: Build Windows installers (EXE & MSI)\n        run: |\n          set -euo pipefail\n          retry() {\n            local n=1 max=3 delay=5\n            while true; do\n              echo \"Attempt #$n: $*\"\n              \"$@\" && break\n              [ $n -ge $max ] && { echo \"Failed after $n attempts.\"; return 1; }\n              n=$((n+1)); echo \"Retrying in ${delay}s...\"; sleep $delay; delay=$((delay*2))\n            done\n          }\n          retry ./gradlew :composeApp:packageExe :composeApp:packageMsi\n        shell: bash\n\n      - name: Upload Windows installers\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-installers\n          path: |\n            composeApp/build/compose/binaries/main/exe/*.exe\n            composeApp/build/compose/binaries/main/msi/*.msi\n          retention-days: 30\n          compression-level: 6\n\n  build-macos:\n    strategy:\n      matrix:\n        include:\n          - os: macos-15-intel\n            arch: x64\n          - os: macos-latest\n            arch: arm64\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Validate Gradle wrapper\n        uses: gradle/actions/wrapper-validation@v4\n\n      - name: Set up JDK ${{ env.JAVA_VERSION }}\n        uses: actions/setup-java@v4\n        with:\n          distribution: ${{ env.JAVA_DISTRIBUTION }}\n          java-version: ${{ env.JAVA_VERSION }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v4\n        with:\n          cache-read-only: false\n          cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}\n          gradle-home-cache-cleanup: true\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Build macOS installers (DMG & PKG)\n        run: |\n          set -euo pipefail\n          retry() {\n            local n=1 max=3 delay=5\n            while true; do\n              echo \"Attempt #$n: $*\"\n              \"$@\" && break\n              [ $n -ge $max ] && { echo \"Failed after $n attempts.\"; return 1; }\n              n=$((n+1)); echo \"Retrying in ${delay}s...\"; sleep $delay; delay=$((delay*2))\n            done\n          }\n          retry ./gradlew :composeApp:packageDmg :composeApp:packagePkg\n        shell: bash\n\n      - name: Upload macOS installers\n        uses: actions/upload-artifact@v4\n        with:\n          name: macos-installers-${{ matrix.arch }}\n          path: |\n            composeApp/build/compose/binaries/main/dmg/*.dmg\n            composeApp/build/compose/binaries/main/pkg/*.pkg\n          retention-days: 30\n          compression-level: 6\n\n  build-linux:\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            label: modern\n            gradle-tasks: >-\n              :composeApp:packageDeb\n              :composeApp:packageRpm\n              :composeApp:packageAppImage\n\n          - os: ubuntu-22.04\n            label: debian12-compat\n            gradle-tasks: >-\n              :composeApp:packageDeb\n              :composeApp:packageRpm\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Validate Gradle wrapper\n        uses: gradle/actions/wrapper-validation@v4\n\n      - name: Set up JDK ${{ env.JAVA_VERSION }}\n        uses: actions/setup-java@v4\n        with:\n          distribution: ${{ env.JAVA_DISTRIBUTION }}\n          java-version: ${{ env.JAVA_VERSION }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v4\n        with:\n          cache-read-only: false\n          cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}\n          gradle-home-cache-cleanup: true\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Build Linux installers\n        run: |\n          set -euo pipefail\n          retry() {\n            local n=1 max=3 delay=5\n            while true; do\n              echo \"Attempt #$n: $*\"\n              \"$@\" && break\n              [ $n -ge $max ] && { echo \"Failed after $n attempts.\"; return 1; }\n              n=$((n+1)); echo \"Retrying in ${delay}s...\"; sleep $delay; delay=$((delay*2))\n            done\n          }\n          retry ./gradlew ${{ matrix.gradle-tasks }}\n        shell: bash\n\n      - name: List AppImage build output\n        if: matrix.label == 'modern'\n        run: |\n          echo \"=== Listing build output ===\"\n          find composeApp/build/compose/binaries/main -maxdepth 3 -type d 2>/dev/null || echo \"Directory not found\"\n          echo \"=== All files ===\"\n          find composeApp/build/compose/binaries/main -maxdepth 4 -type f 2>/dev/null | head -30 || echo \"No files found\"\n        shell: bash\n\n      - name: Build AppImage with appimagetool\n        if: matrix.label == 'modern'\n        run: |\n          set -euo pipefail\n\n          # Find the directory containing the app launcher (bin/GitHub-Store)\n          APP_ROOT=\"\"\n          for candidate in \\\n            composeApp/build/compose/binaries/main/app-image/GitHub-Store \\\n            composeApp/build/compose/binaries/main/app/GitHub-Store \\\n            composeApp/build/compose/binaries/main/app-image \\\n            composeApp/build/compose/binaries/main/app; do\n            if [ -f \"$candidate/bin/GitHub-Store\" ]; then\n              APP_ROOT=\"$candidate\"\n              echo \"Found app root at: $candidate\"\n              break\n            fi\n          done\n\n          if [ -z \"$APP_ROOT\" ]; then\n            echo \"ERROR: Could not find app launcher (bin/GitHub-Store)\"\n            find composeApp/build/compose/binaries/main -type f -name \"GitHub-Store\" 2>/dev/null || true\n            exit 1\n          fi\n\n          # Download appimagetool\n          wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage\n          chmod +x appimagetool-x86_64.AppImage\n\n          # Create AppDir from Compose output\n          APPDIR=\"GitHub-Store.AppDir\"\n          mv \"$APP_ROOT\" \"$APPDIR\"\n\n          # Create AppRun entry point\n          cat > \"$APPDIR/AppRun\" << 'EOF'\n          #!/bin/bash\n          SELF=$(readlink -f \"$0\")\n          HERE=${SELF%/*}\n          exec \"${HERE}/bin/GitHub-Store\" \"$@\"\n          EOF\n          chmod +x \"$APPDIR/AppRun\"\n\n          # Create .desktop file\n          cat > \"$APPDIR/github-store.desktop\" << 'EOF'\n          [Desktop Entry]\n          Type=Application\n          Name=GitHub Store\n          Exec=GitHub-Store\n          Icon=github-store\n          Categories=Development;\n          Comment=Cross-platform app store for GitHub releases\n          EOF\n\n          # Copy icon to AppDir root (required by appimagetool)\n          cp \"$APPDIR/lib/GitHub-Store.png\" \"$APPDIR/github-store.png\"\n\n          # Build .AppImage\n          OUTPUT=\"composeApp/build/compose/binaries/main/GitHub-Store-x86_64.AppImage\"\n          UPINFO=\"gh-releases-zsync|rainxchzed|Github-Store|latest|*x86_64.AppImage.zsync\"\n          ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 ./appimagetool-x86_64.AppImage -u \"$UPINFO\" \"$APPDIR\" \"$OUTPUT\"\n\n          # appimagetool may place .zsync in the working directory; move it next to the AppImage\n          ZSYNC_NAME=\"$(basename \"$OUTPUT\").zsync\"\n          if [ -f \"$ZSYNC_NAME\" ] && [ ! -f \"$OUTPUT.zsync\" ]; then\n            mv \"$ZSYNC_NAME\" \"$OUTPUT.zsync\"\n          fi\n\n          echo \"Created AppImage and zsync:\"\n          ls -lh \"$OUTPUT\" \"$OUTPUT.zsync\"\n        shell: bash\n\n      - name: Upload Linux installers\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-installers-${{ matrix.label }}\n          path: |\n            composeApp/build/compose/binaries/main/deb/*.deb\n            composeApp/build/compose/binaries/main/rpm/*.rpm\n          retention-days: 30\n          compression-level: 6\n\n      - name: Upload Linux AppImage\n        if: matrix.label == 'modern'\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-appimage\n          path: |\n            composeApp/build/compose/binaries/main/GitHub-Store-x86_64.AppImage\n            composeApp/build/compose/binaries/main/GitHub-Store-x86_64.AppImage.zsync\n          if-no-files-found: error\n          retention-days: 30\n          compression-level: 0\n"
  },
  {
    "path": ".gitignore",
    "content": "# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Log/OS Files\n*.log\n\n# Android Studio generated files and folders\ncaptures/\n.externalNativeBuild/\n.cxx/\n*.aab\n*.apk\noutput-metadata.json\n\n# IntelliJ\n*.iml\n.idea/\nmisc.xml\ndeploymentTargetDropDown.xml\nrender.experimental.xml\n\n# Keystore files\n*.jks\n*.keystore\n\n# Google Services (e.g. APIs or Firebase)\ngoogle-services.json\n\n# Android Profiling\n*.hprof\n.kotlin/\n\ncomposeApp/release/baselineProfiles/\n\ncomposeApp/kotzilla.json"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md - GitHub Store\n\n## Project Overview\n\nGitHub Store is a cross-platform app store for GitHub releases, built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. Targets **Android** (min API 26) and **Desktop** (Windows, macOS, Linux via JVM).\n\nPackage: `zed.rainxch.githubstore` | Version: 1.6.2 (code 13) | Target SDK: 36\n\n## Build & Run Commands\n\n```bash\n# Android\n./gradlew :composeApp:assembleDebug\n./gradlew :composeApp:assembleRelease\n\n# Desktop (run in dev mode)\n./gradlew :composeApp:run\n\n# Desktop installers\n./gradlew :composeApp:packageExe :composeApp:packageMsi   # Windows\n./gradlew :composeApp:packageDmg :composeApp:packagePkg   # macOS\n./gradlew :composeApp:packageDeb :composeApp:packageRpm   # Linux\n\n# Full build check\n./gradlew build\n```\n\n**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.\n\n## Project Structure\n\n```\ncomposeApp/                          # Main app module (entry points, navigation, DI wiring)\n  src/commonMain/                    # Shared UI & app wiring\n  src/androidMain/                   # Android entry point (MainActivity)\n  src/jvmMain/                       # Desktop entry point (DesktopApp.kt)\ncore/\n  domain/                            # Shared interfaces, models, use cases (no framework deps)\n  data/                              # Shared repos, networking (Ktor), database (Room), DI\n  presentation/                      # Shared theming (Material 3) & reusable UI components\nfeature/\n  apps/                              # Installed applications management\n  auth/                              # GitHub OAuth device flow authentication\n  details/                           # Repository details, releases, readme, downloads\n  dev-profile/                       # Developer/user profile display\n  favourites/                        # Saved favorite repositories (presentation-only)\n  home/                              # Main discovery screen (trending, hot, popular)\n  profile/                           # User profile, settings, appearance, proxy, Shizuku installer\n  search/                            # Repository search with filters\n  starred/                           # Starred repositories (presentation-only)\nbuild-logic/convention/              # Custom Gradle convention plugins\n```\n\nEach feature has up to 3 sub-modules: `domain/` (interfaces & models), `data/` (implementations & DI), `presentation/` (screens & ViewModels). Some features (favourites, starred) are presentation-only and use core repositories directly.\n\n## Architecture\n\n**Clean Architecture + MVVM** with strict layer separation per feature module:\n\n- **Domain** - Repository interfaces, models, use cases (no framework dependencies)\n- **Data** - Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers\n- **Presentation** - ViewModels with `StateFlow`/`Channel`, Compose screens\n\n### State Management Pattern\n\nEvery screen follows the same State/Action/Event pattern:\n\n```kotlin\nclass XViewModel : ViewModel() {\n    private val _state = MutableStateFlow(XState())\n    val state = _state.asStateFlow()  // or .stateIn() with WhileSubscribed\n\n    private val _events = Channel<XEvent>()\n    val events = _events.receiveAsFlow()\n\n    fun onAction(action: XAction) { ... }\n}\n```\n\n- `State` - data class holding all UI state\n- `Action` - sealed interface for user input (clicks, refreshes, etc.)\n- `Event` - sealed interface for one-off effects (navigation, toasts, scroll)\n\n### Navigation\n\nType-safe navigation using `@Serializable` sealed interface `GithubStoreGraph`:\n\n```\nHomeScreen, SearchScreen, AuthenticationScreen, ProfileScreen,\nFavouritesScreen, StarredReposScreen, AppsScreen, SponsorScreen\nDetailsScreen(repositoryId, owner, repo, isComingFromUpdate)\nDeveloperProfileScreen(username)\n```\n\nRoutes defined in `composeApp/.../app/navigation/GithubStoreGraph.kt`, wired in `AppNavigation.kt`.\n\n### Dependency Injection\n\n**Koin** - modules defined in each feature's `data/di/SharedModule.kt`, registered in `composeApp/.../app/di/initKoin.kt`. ViewModels injected via `koinViewModel()`.\n\n### Core Modules\n\n| Module | Purpose | Key Contents |\n|--------|---------|--------------|\n| `core/domain` | Shared contracts | Repository interfaces (`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`), models (`GithubRepoSummary`, `GithubRelease`, `InstalledApp`, `ProxyConfig`, `InstallerType`, `ShizukuAvailability`), system interfaces (`Installer`, `InstallerInfoExtractor`, `InstallerStatusProvider`, `PackageMonitor`) |\n| `core/data` | Shared implementations | `HttpClientFactory` (Ktor + interceptors), `AppDatabase` (Room), `ProxyManager`, `TokenStore`, `LocalizationManager`, platform-specific clients (OkHttp for Android, CIO for Desktop), Shizuku integration (Android: `ShizukuServiceManager`, `ShizukuInstallerWrapper`, `ShizukuInstallerServiceImpl`, `AndroidInstallerStatusProvider`; Desktop: `DesktopInstallerStatusProvider`) |\n| `core/presentation` | Shared UI | `GithubStoreTheme` (Material 3), reusable components (`RepositoryCard`, `GithubStoreButton`), formatting utils, localized strings (11 languages) |\n\n## Tech Stack\n\n| Area | Library | Version |\n|------|---------|---------|\n| Language | Kotlin | 2.3.10 |\n| UI | Compose Multiplatform | 1.10.1 |\n| HTTP | Ktor | 3.4.0 |\n| Database | Room | 2.8.4 |\n| DI | Koin | 4.1.1 |\n| Serialization | Kotlinx Serialization | 1.10.0 |\n| Preferences | DataStore | 1.2.0 |\n| Image Loading | Landscapist (Coil3) | 2.9.5 |\n| Logging | Kermit | 2.0.8 |\n| Permissions | MOKO Permissions | 0.20.1 |\n| Navigation | Navigation Compose | 2.9.2 |\n| Markdown | Multiplatform Markdown Renderer | 0.39.2 |\n| Shizuku | Shizuku API | 13.1.5 |\n| Background Work | WorkManager | 2.11.1 |\n| Date/Time | Kotlinx Datetime | 0.7.1 |\n\nAll versions managed in `gradle/libs.versions.toml` (Version Catalog).\n\n## Convention Plugins\n\nCustom Gradle plugins in `build-logic/convention/` standardize module setup:\n\n| Plugin | Use For |\n|--------|---------|\n| `convention.kmp.library` | KMP shared library modules (domain, data) |\n| `convention.cmp.library` | Compose Multiplatform library modules (core/presentation) |\n| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |\n| `convention.cmp.application` | Main app module |\n| `convention.room` | Room database modules |\n| `convention.buildkonfig` | Build-time config (API keys from local.properties) |\n\n## Adding a New Feature\n\n1. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`\n2. Add `build.gradle.kts` in each using the appropriate convention plugin\n3. Add `include` entries in `settings.gradle.kts`\n4. Define domain interfaces/models in `domain/`\n5. Implement repository + Koin DI module in `data/di/SharedModule.kt`\n6. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`\n7. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`\n8. Register the Koin module in `initKoin.kt`\n\n## Key Configuration\n\n- **GitHub OAuth:** Set `GITHUB_CLIENT_ID` in `local.properties`. Callback URL: `githubstore://callback`. Deep link: `githubstore://repo`\n- **Shizuku (Android):** Optional silent install via `ShizukuProvider` (registered in AndroidManifest). Requires Shizuku app running with ADB or root. AIDL service passes APK via `ParcelFileDescriptor` to `pm install -S`. Falls back to standard installer on failure.\n- **Gradle properties:** Config cache enabled, build cache enabled, 4GB Gradle heap, 3GB Kotlin daemon heap\n- **Code style:** Official Kotlin style (`kotlin.code.style=official`)\n\n## Coding Conventions\n\n- Packages follow `zed.rainxch.{module}.{layer}` pattern\n- Private state properties use underscore prefix: `_state`\n- Sealed classes/interfaces for type-safe navigation routes, actions, events\n- Repository pattern: interface in `domain/`, implementation in `data/`\n- Composition over inheritance via Koin DI\n- Source sets: `commonMain` for shared, `androidMain` for Android, `jvmMain` for Desktop\n- Feature CLAUDE.md files exist in each `feature/` directory for module-specific guidance\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nrainxchzed@gmail.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## How to contribute\n\nWe'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow.\n\n## Preparing a pull request for review\n\nBefore opening a pull request, please:\n\n- Make sure the code builds on all supported targets (Android + Desktop).\n- Add or update tests where it makes sense.\n- Keep changes focused and reasonably small.\n\nIf you introduce new dependencies or modules, briefly explain why in the PR description.\n\n## Code reviews\n\nAll submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult\n[GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)\nfor more information on using pull requests.\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n  \n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n \n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Project Overview\n\nGitHub Store is a cross-platform app store for GitHub releases, designed to simplify discovering and installing open-source software. It automatically detects installable binaries (APK, EXE, DMG, AppImage, DEB, RPM), provides one-click installation, tracks updates, and presents repository information in a clean, app-store style interface.\n\nBuilt with Kotlin Multiplatform and Compose Multiplatform for Android and Desktop platforms.\n\n</div>\n\n<div align=\"center\"><a href=\"https://github.com/Safouene1/support-palestine-banner/blob/master/Markdown-pages/Support.md\"><img src=\"https://raw.githubusercontent.com/Safouene1/support-palestine-banner/master/banner-project.svg\" alt=\"Support Palestine\" style=\"width: 100%;\"></a></div>\n\n> [!CAUTION]\n> Free and Open-Source Android is under threat. Google will turn Android into a locked-down platform, restricting your essential freedom to install apps of your choice. Make your voice heard – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki & Resources\n\nCheck out GitHub Store [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) for FAQ and useful information\n\n🌐 **Website:** [github-store.org](https://github-store.org)\n💬 **Discord:** [Join the community](https://discord.gg/x9Cvh2Z9qS)\n📜 **Privacy Policy:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Legal Notice\n\nGitHub Store is an independent, open-source project not affiliated with GitHub, Inc.  \nThe name describes the app's functionality (discovering GitHub releases) and does not imply trademark ownership.  \nGitHub® is a registered trademark of GitHub, Inc.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 Download\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS Users:** You may see a warning that Apple cannot verify GitHub Store. This happens because the app is distributed outside the App Store and is not notarized yet. Allow it via System Settings → Privacy & Security → Open Anyway.\n\n---\n\n<p align=\"center\">\n\n# 🏆 Featured In\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">Top 20 Best Android Apps 2026</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Top 12 App Stores that are Better than Google Play Store </a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Featured Project</a>\n</p>\n\n---\n\n## 🚀 Features\n\n- **Smart discovery**\n    - Home sections for “Trending”, “Hot Release”, and “Most Popular” projects with time‑based filters.\n    - Only repos with valid installable assets are shown.\n    - Platform‑aware topic scoring so Android/desktop users see relevant apps first.\n    - Overhauled search with improved relevance ranking and performance.\n\n- **Release browser & installs**\n    - Release picker to browse and install from any release, not just the latest.\n    - Fetches all releases for each repository.\n    - Single “Install latest” action, plus an expandable list of all available releases and their installers.\n    - Manual install option with automatic compatibility checks.\n\n- **Rich details screen**\n    - App name, version and share action.\n    - Stars, forks, open issues.\n    - Rendered README content (“About this app”).\n    - Release notes with Markdown formatting for any selected release.\n    - List of installers with platform labels and file sizes.\n    - Deep linking support — open repository details directly via URL.\n    - Developer profile screen to explore a developer’s repositories and activity.\n\n- **App management**\n    - Open, uninstall, and downgrade installed apps directly from GitHub Store.\n    - Android: APK architecture matching (armv7/armv8), package monitoring, and update tracking.\n    - Desktop (Windows/macOS/Linux): downloads installers to the user’s Downloads folder and opens them with the default handler.\n\n- **Starred repositories**\n    - Save and browse your starred GitHub repositories from within the app.\n\n- **Network & performance**\n    - Dynamic proxy support for configurable network routing.\n    - Enhanced caching system for faster loading and reduced API usage.\n---\n\n## 🔍 How does my app appear in GitHub Store?\n\nGitHub Store does not use any private indexing or manual curation rules.  \nYour project can appear automatically if it follows these conditions:\n\n1. **Public repository on GitHub**\n    - Visibility must be `public`.\n\n2. **Installable assets in the latest release**\n    - The latest release must contain at least one asset file with a supported extension:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store ignores GitHub’s auto‑generated source artifacts (`Source code (zip)` /\n      `Source code (tar.gz)`).\n\n3. **Discoverable by search / topics**\n    - Repositories are fetched via the public GitHub Search API.\n    - Topic, language, and description help the ranking:\n        - Android apps: topics like `android`, `mobile`, `apk`.\n        - Desktop apps: topics like `desktop`, `windows`, `linux`, `macos`, `compose-desktop`,\n          `electron`.\n    - Having at least a few stars makes it more likely to appear under Trending/Hot Release/Most Popular sections.\n\nIf your repo meets these conditions, GitHub Store can find it through search and show it\nautomatically—no manual submission required.\n\n---\n\n## ✅ Pros / Why use GitHub Store?\n\n- **No more hunting through GitHub releases**\n  See only repos that actually ship binaries for your platform.\n\n- **Knows what you installed**\n  Tracks apps installed via GitHub Store (Android) and highlights when new releases are available, so you can update them without hunting through GitHub again.\n\n- **Always up to date**\n  Installs default to the latest published release, with the option to browse and install from\n  any previous release via the release picker.\n\n- **Open source & extensible**  \n  Written in KMP with a clear separation between networking, domain logic, and UI—easy to fork,\n  extend, or adapt.\n\n---\n\n## 🔐 GitHub Store APK Signing Certificate\n\nAll official GitHub Store releases are signed with the following certificate fingerprint:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth Configuration\n\n**TL;DR**\n1. Create a GitHub OAuth App  \n2. Copy **Client ID**  \n3. Put it in `local.properties`\n\n<details>\n<summary><strong>Show full setup guide</strong></summary>\n\n  <br/>\n  \n### 1 - Create a GitHub OAuth App\nGo to:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Field                          | Value                                       |\n| ------------------------------ | ------------------------------------------- |\n| **Application name**           | Anything you like (e.g. *GitHub Store Dev*) |\n| **Homepage URL** | `https://github.com/username/repo_name`                   |\n| **Authorization callback URL** | `githubstore://callback`                    |\n\nThen click **Create application**.\n\n### 2 - Copy Your Client ID\nAfter creating the app, GitHub will show:\n- **Client ID** ← you need this\n- **Client Secret** ← ❗ NOT required for this project\n\n### 3 - Add It to Your Project\nOpen your project’s `local.properties` file (root of the project) and add:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Sync & Run\nSync the project and run the app. You should now be able to sign in with GitHub.\n\n### ❗ Important Notes\n- `local.properties` is **not committed to Git**, so your Client ID stays local.\n- This project only needs the **Client ID** (not the Client Secret).\n- Each developer should create their own OAuth app for development.\n\n</details>\n\n---\n\n## ☕ Support the project\n\nGitHub Store is built and maintained by high school student. Your support helps him:\n\n✅ **Keep the app bug-free** — respond to issues and ship fixes quickly  \n✅ **Add community-requested features** — implement what users actually need  \n\n### 💖 Ways to Support\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**Can't sponsor right now?** That's okay! You can still help by:\n- ⭐ **Starring this repo** — helps others discover GitHub Store\n- 🐛 **Reporting bugs** — makes the app better for everyone\n- 📢 **Sharing with friends** — spread the word to other developers and tech and non-tech buddies!\n- 💬 **Joining our [Discord](https://discord.gg/x9Cvh2Z9qS)** — your feedback shapes the roadmap\n\nEvery bit of support—financial or not—means the world and keeps this project alive. Thank you!\n\n---\n\n## ⚠️ Disclaimer\n\nGitHub Store only helps you discover and download release assets that are already published on\nGitHub by third‑party developers.  \nThe contents, safety, and behavior of those downloads are entirely the responsibility of their\nrespective authors and distributors, not this project.\n\nBy using GithubStore, you understand and agree that you install and run any downloaded software at\nyour own risk.  \nThis project does not review, validate, or guarantee that any installer is safe, free of malware, or\nfit for any particular purpose.\n\n---\n\n## Star History\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 License\n\nGitHub Store will be released under the **Apache License, Version 2.0**.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n"
  },
  {
    "path": "SECURITY.md",
    "content": "## Security Policy\n\n## Reporting a Vulnerability\n\nWe take the security of this repository seriously. If you discover a security vulnerability, please report it responsibly.\n\n**Please do not open a public GitHub issue for security vulnerabilities.**\n\nInstead, use one of the following methods:\n\n- **GitHub Security Advisories**  \n  Use the \"Report a vulnerability\" feature available in the repository’s **Security** tab.\n\n- **Email**  \n  Send a detailed report to: [rainxch.dev@gmail.com](mailto:rainxch.dev@gmail.com)\n\n---\n\n## What to Include in Your Report\n\nTo help us assess and resolve the issue quickly, please include:\n\n- A clear description of the vulnerability\n- Steps to reproduce the issue\n- Proof of concept (PoC), if available\n- Affected files, endpoints, or components\n- Potential impact of the vulnerability\n\n---\n\n## Response Timeline\n\nWe aim to follow this general timeline:\n\n- **Acknowledgement:** Within 48 hours\n- **Initial assessment:** Within 5 business days\n- **Fix or mitigation:** Based on severity and complexity\n\nTimelines may vary depending on the nature of the vulnerability.\n\n---\n\n## Coordinated Disclosure\n\nWe kindly request that you practice responsible disclosure and avoid sharing details publicly until the vulnerability has been addressed or a fix has been released.\n\n---\n\n## Security Best Practices\n\nContributors are encouraged to:\n\n- Follow secure coding practices\n- Avoid committing secrets or credentials\n- Use dependency scanning and security tools where possible\n- Review pull requests for potential security issues\n\n---\n\n## Thank You\n\nWe appreciate the efforts of the security community and responsible researchers who help keep this project secure.\n\n"
  },
  {
    "path": "build-logic/convention/build.gradle.kts",
    "content": "import org.gradle.kotlin.dsl.compileOnly\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    `kotlin-dsl`\n}\n\ngroup = \"zed.rainxch.convention.buildlogic\"\n\ndependencies {\n    compileOnly(libs.android.gradlePlugin)\n    compileOnly(libs.android.tools.common)\n    compileOnly(libs.kotlin.gradlePlugin)\n    compileOnly(libs.compose.gradlePlugin)\n    compileOnly(libs.androidx.room.gradle.plugin)\n\n    implementation(libs.buildkonfig.gradlePlugin)\n    implementation(libs.buildkonfig.compiler)\n\n    implementation(libs.ktlint.gradlePlugin)\n}\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget = JvmTarget.JVM_17\n    }\n}\n\ntasks {\n    validatePlugins {\n        enableStricterValidation = true\n        failOnWarning = true\n    }\n}\n\ngradlePlugin {\n    plugins {\n        register(\"androidApplication\") {\n            id = \"zed.rainxch.convention.android.application\"\n            implementationClass = \"AndroidApplicationConventionPlugin\"\n        }\n        register(\"androidComposeApplication\") {\n            id = \"zed.rainxch.convention.android.application.compose\"\n            implementationClass = \"AndroidApplicationComposeConventionPlugin\"\n        }\n        register(\"cmpApplication\") {\n            id = \"zed.rainxch.convention.cmp.application\"\n            implementationClass = \"CmpApplicationConventionPlugin\"\n        }\n        register(\"kmpLibrary\") {\n            id = \"zed.rainxch.convention.kmp.library\"\n            implementationClass = \"KmpLibraryConventionPlugin\"\n        }\n        register(\"cmpLibrary\") {\n            id = \"zed.rainxch.convention.cmp.library\"\n            implementationClass = \"CmpLibraryConventionPlugin\"\n        }\n        register(\"cmpFeature\") {\n            id = \"zed.rainxch.convention.cmp.feature\"\n            implementationClass = \"CmpFeatureConventionPlugin\"\n        }\n        register(\"buildKonfig\") {\n            id = \"zed.rainxch.convention.buildkonfig\"\n            implementationClass = \"BuildKonfigConventionPlugin\"\n        }\n        register(\"room\") {\n            id = \"zed.rainxch.convention.room\"\n            implementationClass = \"RoomConventionPlugin\"\n        }\n        register(\"ktlint\") {\n            id = \"zed.rainxch.convention.ktlint\"\n            implementationClass = \"KtlintConventionPlugin\"\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt",
    "content": "import com.android.build.api.dsl.ApplicationExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.getByType\nimport zed.rainxch.githubstore.convention.configureAndroidCompose\n\nclass AndroidApplicationComposeConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"zed.rainxch.convention.android.application\")\n                apply(\"org.jetbrains.kotlin.plugin.compose\")\n                apply(\"zed.rainxch.convention.ktlint\")\n            }\n\n            val commonExtension = extensions.getByType<ApplicationExtension>()\n            configureAndroidCompose(commonExtension)\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt",
    "content": "import com.android.build.api.dsl.ApplicationExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport zed.rainxch.githubstore.convention.configureKotlinAndroid\nimport zed.rainxch.githubstore.convention.libs\n\nclass AndroidApplicationConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.android.application\")\n            }\n\n            extensions.configure<ApplicationExtension> {\n                namespace = \"zed.rainxch.githubstore\"\n                compileSdk =\n                    libs\n                        .findVersion(\"projectCompileSdkVersion\")\n                        .get()\n                        .toString()\n                        .toInt()\n\n                defaultConfig {\n                    applicationId = libs.findVersion(\"projectApplicationId\").get().toString()\n                    minSdk =\n                        libs\n                            .findVersion(\"projectMinSdkVersion\")\n                            .get()\n                            .toString()\n                            .toInt()\n                    targetSdk =\n                        libs\n                            .findVersion(\"projectTargetSdkVersion\")\n                            .get()\n                            .toString()\n                            .toInt()\n                    versionCode =\n                        libs\n                            .findVersion(\"projectVersionCode\")\n                            .get()\n                            .toString()\n                            .toInt()\n                    versionName = libs.findVersion(\"projectVersionName\").get().toString()\n                }\n                packaging {\n                    resources {\n                        excludes += \"/META-INF/{AL2.0,LGPL2.1}\"\n                    }\n                }\n                buildTypes {\n                    getByName(\"release\") {\n                        isMinifyEnabled = true\n                        isShrinkResources = true\n\n                        proguardFiles(\n                            getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                            \"proguard-rules.pro\",\n                        )\n                    }\n                }\n\n                configureKotlinAndroid(this)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/BuildKonfigConventionPlugin.kt",
    "content": "import com.codingfeline.buildkonfig.compiler.FieldSpec\nimport com.codingfeline.buildkonfig.gradle.BuildKonfigExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport zed.rainxch.githubstore.convention.libs\nimport zed.rainxch.githubstore.convention.pathToPackageName\nimport java.util.Properties\n\nclass BuildKonfigConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.codingfeline.buildkonfig\")\n            }\n\n            extensions.configure<BuildKonfigExtension> {\n                packageName = target.pathToPackageName()\n\n                defaultConfigs {\n                    val localProps =\n                        Properties().apply {\n                            val file = rootProject.file(\"local.properties\")\n                            if (file.exists()) file.inputStream().use { this.load(it) }\n                        }\n\n                    val githubClientId =\n                        (\n                            localProps.getProperty(\"GITHUB_CLIENT_ID\")\n                                ?: \"Ov23linTY28VFpFjFiI9\"\n                        ).trim()\n\n                    val versionName = libs.findVersion(\"projectVersionName\").get().toString()\n\n                    buildConfigField(FieldSpec.Type.STRING, \"GITHUB_CLIENT_ID\", githubClientId)\n                    buildConfigField(FieldSpec.Type.STRING, \"VERSION_NAME\", versionName)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/CmpApplicationConventionPlugin.kt",
    "content": "import org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.configureAndroidTarget\nimport zed.rainxch.githubstore.convention.configureJvmTarget\nimport zed.rainxch.githubstore.convention.libs\n\nclass CmpApplicationConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"zed.rainxch.convention.android.application.compose\")\n                apply(\"org.jetbrains.kotlin.multiplatform\")\n                apply(\"org.jetbrains.compose\")\n                apply(\"org.jetbrains.kotlin.plugin.compose\")\n            }\n\n            configureAndroidTarget()\n            configureJvmTarget()\n\n            dependencies {\n                \"debugImplementation\"(libs.findLibrary(\"androidx-compose-ui-tooling\").get())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/CmpFeatureConventionPlugin.kt",
    "content": "import org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.libs\n\nclass CmpFeatureConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"zed.rainxch.convention.cmp.library\")\n            }\n\n            dependencies {\n                \"commonMainImplementation\"(project(\":core:presentation\"))\n\n                \"commonMainImplementation\"(platform(libs.findLibrary(\"koin-bom\").get()))\n                \"androidMainImplementation\"(platform(libs.findLibrary(\"koin-bom\").get()))\n\n                \"commonMainImplementation\"(libs.findLibrary(\"koin-compose\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"koin-compose-viewmodel\").get())\n\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-runtime\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-viewmodel\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-lifecycle-viewmodel\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-lifecycle-compose\").get())\n\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-lifecycle-viewmodel-savedstate\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-savedstate\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-bundle\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-navigation\").get())\n\n                \"androidMainImplementation\"(libs.findLibrary(\"koin-android\").get())\n                \"androidMainImplementation\"(libs.findLibrary(\"koin-androidx-compose\").get())\n                \"androidMainImplementation\"(libs.findLibrary(\"koin-androidx-navigation\").get())\n                \"androidMainImplementation\"(libs.findLibrary(\"koin-core-viewmodel\").get())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/CmpLibraryConventionPlugin.kt",
    "content": "import org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.libs\nimport kotlin.text.get\n\nclass CmpLibraryConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"zed.rainxch.convention.kmp.library\")\n                apply(\"org.jetbrains.kotlin.plugin.compose\")\n                apply(\"org.jetbrains.compose\")\n            }\n\n            dependencies {\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-ui\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-foundation\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-material3\").get())\n                \"commonMainImplementation\"(libs.findLibrary(\"jetbrains-compose-material-icons-extended\").get())\n\n                \"debugImplementation\"(libs.findLibrary(\"androidx-compose-ui-tooling\").get())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt",
    "content": "import com.android.build.api.dsl.LibraryExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.configureKotlinAndroid\nimport zed.rainxch.githubstore.convention.configureKotlinMultiplatform\nimport zed.rainxch.githubstore.convention.libs\nimport zed.rainxch.githubstore.convention.pathToResourcePrefix\n\nclass KmpLibraryConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.android.library\")\n                apply(\"org.jetbrains.kotlin.multiplatform\")\n                apply(\"org.jetbrains.kotlin.plugin.serialization\")\n            }\n\n            configureKotlinMultiplatform()\n\n            extensions.configure<LibraryExtension> {\n                configureKotlinAndroid(this)\n\n                resourcePrefix = this@with.pathToResourcePrefix()\n\n                experimentalProperties[\"android.experimental.kmp.enableAndroidResources\"] = \"true\"\n            }\n\n            dependencies {\n                \"commonMainImplementation\"(libs.findLibrary(\"kotlinx-serialization-json\").get())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/KtlintConventionPlugin.kt",
    "content": "import org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.jlleitschuh.gradle.ktlint.KtlintExtension\n\nclass KtlintConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            pluginManager.apply(\"org.jlleitschuh.gradle.ktlint\")\n\n            extensions.configure(KtlintExtension::class.java) {\n                version.set(\"1.8.0\")\n                outputToConsole.set(true)\n                ignoreFailures.set(true)\n                filter {\n                    exclude(\"**/generated/**\")\n                    exclude(\"**/build/**\")\n                    exclude(\"**/*.g.kt\")\n                    exclude(\"**/schemas/**\")\n                }\n                reporters {\n                    reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)\n                    reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.HTML)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/RoomConventionPlugin.kt",
    "content": "import androidx.room.gradle.RoomExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.libs\n\nclass RoomConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.google.devtools.ksp\")\n                apply(\"androidx.room\")\n            }\n\n            extensions.configure<RoomExtension> {\n                schemaDirectory(\"$projectDir/schemas\")\n            }\n\n            dependencies {\n                \"commonMainApi\"(libs.findLibrary(\"androidx-room-runtime\").get())\n                \"commonMainApi\"(libs.findLibrary(\"sqlite-bundled\").get())\n                \"kspAndroid\"(libs.findLibrary(\"androidx-room-compiler\").get())\n                \"kspJvm\"(libs.findLibrary(\"androidx-room-compiler\").get())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/AndroidCompose.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport com.android.build.api.dsl.CommonExtension\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\n\nfun Project.configureAndroidCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) {\n    with(commonExtension) {\n        buildFeatures {\n            compose = true\n        }\n\n        dependencies {\n            val composeBom = libs.findLibrary(\"androidx-compose-bom\").get()\n\n            \"implementation\"(platform(composeBom))\n            \"testImplementation\"(platform(composeBom))\n            \"debugImplementation\"(libs.findLibrary(\"androidx-compose-ui-tooling-preview\").get())\n            \"debugImplementation\"(libs.findLibrary(\"androidx-compose-ui-tooling\").get())\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/KotlinAndroid.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport com.android.build.api.dsl.CommonExtension\nimport org.gradle.api.JavaVersion\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\nimport org.gradle.kotlin.dsl.withType\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\ninternal fun Project.configureKotlinAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) {\n    with(commonExtension) {\n        compileSdk =\n            libs\n                .findVersion(\"projectCompileSdkVersion\")\n                .get()\n                .toString()\n                .toInt()\n\n        defaultConfig.minSdk =\n            libs\n                .findVersion(\"projectMinSdkVersion\")\n                .get()\n                .toString()\n                .toInt()\n\n        compileOptions {\n            sourceCompatibility = JavaVersion.VERSION_17\n            targetCompatibility = JavaVersion.VERSION_17\n            isCoreLibraryDesugaringEnabled = true\n        }\n\n        configureKotlin()\n\n        dependencies {\n            \"coreLibraryDesugaring\" {\n                libs.findLibrary(\"android-desugarJdkLibs\").get()\n            }\n        }\n    }\n}\n\ninternal fun Project.configureKotlin() {\n    tasks.withType<KotlinCompile>().configureEach {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_17)\n\n            freeCompilerArgs.add(\n                \"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/KotlinAndroidTarget.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\n\ninternal fun Project.configureAndroidTarget() {\n    extensions.configure<KotlinMultiplatformExtension> {\n        androidTarget {\n            compilerOptions {\n                jvmTarget.set(JvmTarget.JVM_17)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/KotlinJvmTarget.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.gradle.kotlin.dsl.getting\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\nimport java.util.Properties\n\ninternal fun Project.configureJvmTarget() {\n    extensions.configure<KotlinMultiplatformExtension> {\n        jvm()\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/KotlinMultiplatform.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport com.android.build.api.dsl.LibraryExtension\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\n\ninternal fun Project.configureKotlinMultiplatform() {\n    extensions.configure<LibraryExtension> {\n        namespace = this@configureKotlinMultiplatform.pathToPackageName()\n    }\n\n    configureAndroidTarget()\n    configureJvmTarget()\n\n    extensions.configure<KotlinMultiplatformExtension> {\n        compilerOptions {\n            freeCompilerArgs.add(\"-Xexpect-actual-classes\")\n            freeCompilerArgs.add(\"-Xmulti-dollar-interpolation\")\n            freeCompilerArgs.add(\"-opt-in=kotlin.RequiresOptIn\")\n            freeCompilerArgs.add(\"-opt-in=kotlin.time.ExperimentalTime\")\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/PathUtil.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport org.gradle.api.Project\nimport java.util.Locale\n\nfun Project.pathToPackageName(): String {\n    val relativePackageName =\n        path\n            .replace(\":\", \".\")\n            .replace(\"-\", \"_\")\n            .lowercase()\n\n    return \"zed.rainxch$relativePackageName\"\n}\n\nfun Project.pathToResourcePrefix(): String =\n    path\n        .replace(\":\", \"_ \")\n        .lowercase()\n        .drop(1) + \"_\"\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/zed/rainxch/githubstore/convention/ProjectExt.kt",
    "content": "package zed.rainxch.githubstore.convention\n\nimport org.gradle.api.Project\nimport org.gradle.api.artifacts.VersionCatalog\nimport org.gradle.api.artifacts.VersionCatalogsExtension\nimport org.gradle.kotlin.dsl.getByType\n\nval Project.libs: VersionCatalog\n    get() = extensions.getByType<VersionCatalogsExtension>().named(\"libs\")\n"
  },
  {
    "path": "build-logic/gradle.properties",
    "content": "org.gradle.parallel=true\norg.gradle.caching=true\norg.gradle.configureondemand=true\n"
  },
  {
    "path": "build-logic/settings.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nrootProject.name = \"build-logic\"\n\ndependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n\n    versionCatalogs {\n        create(\"libs\") {\n            from(files(\"../gradle/libs.versions.toml\"))\n        }\n    }\n}\n\ninclude(\":convention\")\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n    id(\"io.github.jwharm.flatpak-gradle-generator\") version \"1.7.0\"\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.android.library) apply false\n    alias(libs.plugins.compose.hot.reload) apply false\n    alias(libs.plugins.compose.multiplatform) apply false\n    alias(libs.plugins.compose.compiler) apply false\n    alias(libs.plugins.kotlin.multiplatform) apply false\n    alias(libs.plugins.android.kotlin.multiplatform.library) apply false\n    alias(libs.plugins.kotlin.serialization) apply false\n    alias(libs.plugins.ksp) apply false\n    alias(libs.plugins.room) apply false\n}\n\ntasks.named<io.github.jwharm.flatpakgradlegenerator.FlatpakGradleGeneratorTask>(\"flatpakGradleGenerator\") {\n    outputFile.set(layout.buildDirectory.file(\"flatpak-sources.json\"))\n}\n\nsubprojects {\n    afterEvaluate {\n        tasks.configureEach {\n            when (name) {\n                \"preBuild\",\n                \"compileKotlinJvm\",\n                \"compileKotlinAndroid\",\n                -> {\n                    val ktlintFormat = tasks.findByName(\"ktlintFormat\")\n                    if (ktlintFormat != null) dependsOn(ktlintFormat)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/build.gradle.kts",
    "content": "import org.jetbrains.compose.desktop.application.dsl.TargetFormat\n\nplugins {\n    alias(libs.plugins.convention.cmp.application)\n    alias(libs.plugins.kotlin.serialization)\n    alias(libs.plugins.compose.hot.reload)\n}\n\nandroid {\n    dependenciesInfo {\n        includeInApk = false\n        includeInBundle = false\n    }\n}\n\nkotlin {\n    sourceSets {\n        androidMain.dependencies {\n            implementation(libs.androidx.compose.ui.tooling.preview)\n            implementation(libs.androidx.activity.compose)\n\n            implementation(libs.core.splashscreen)\n\n            implementation(libs.koin.android)\n        }\n        commonMain.dependencies {\n            implementation(projects.core.data)\n            implementation(projects.core.domain)\n            implementation(projects.core.presentation)\n\n            implementation(projects.feature.apps.domain)\n            implementation(projects.feature.apps.data)\n            implementation(projects.feature.apps.presentation)\n\n            implementation(projects.feature.auth.domain)\n            implementation(projects.feature.auth.data)\n            implementation(projects.feature.auth.presentation)\n\n            implementation(projects.feature.details.domain)\n            implementation(projects.feature.details.data)\n            implementation(projects.feature.details.presentation)\n\n            implementation(projects.feature.devProfile.domain)\n            implementation(projects.feature.devProfile.data)\n            implementation(projects.feature.devProfile.presentation)\n\n            implementation(projects.feature.favourites.domain)\n            implementation(projects.feature.favourites.data)\n            implementation(projects.feature.favourites.presentation)\n\n            implementation(projects.feature.home.domain)\n            implementation(projects.feature.home.data)\n            implementation(projects.feature.home.presentation)\n\n            implementation(projects.feature.search.domain)\n            implementation(projects.feature.search.data)\n            implementation(projects.feature.search.presentation)\n\n            implementation(projects.feature.profile.domain)\n            implementation(projects.feature.profile.data)\n            implementation(projects.feature.profile.presentation)\n\n            implementation(projects.feature.starred.domain)\n            implementation(projects.feature.starred.data)\n            implementation(projects.feature.starred.presentation)\n\n            implementation(libs.jetbrains.compose.navigation)\n            implementation(libs.bundles.koin.common)\n            implementation(libs.liquid)\n            implementation(libs.jetbrains.compose.material.icons.extended)\n\n            implementation(libs.touchlab.kermit)\n            implementation(libs.kotlinx.collections.immutable)\n\n            implementation(compose.runtime)\n            implementation(compose.foundation)\n            implementation(libs.jetbrains.compose.material3)\n            implementation(compose.ui)\n            implementation(compose.components.resources)\n\n            implementation(libs.androidx.compose.ui.tooling.preview)\n            implementation(libs.jetbrains.compose.viewmodel)\n            implementation(libs.jetbrains.lifecycle.compose)\n        }\n\n        jvmMain {\n            dependencies {\n                implementation(projects.core.presentation)\n                implementation(compose.desktop.currentOs)\n                implementation(libs.kotlinx.coroutines.swing)\n                implementation(libs.kotlin.stdlib)\n                implementation(libs.koin.compose)\n                implementation(libs.koin.compose.viewmodel)\n                implementation(libs.koin.compose.viewmodel)\n\n                implementation(compose.desktop.linux_x64)\n                implementation(compose.desktop.linux_arm64)\n                implementation(compose.desktop.macos_x64)\n                implementation(compose.desktop.macos_arm64)\n                implementation(compose.desktop.windows_x64)\n                implementation(compose.desktop.windows_arm64)\n\n                implementation(libs.slf4j.simple)\n            }\n        }\n    }\n}\n\ncompose.desktop {\n    application {\n        mainClass = \"zed.rainxch.githubstore.DesktopAppKt\"\n        nativeDistributions {\n            packageName = \"GitHub-Store\"\n            packageVersion =\n                libs.versions.projectVersionName\n                    .get()\n                    .toString()\n            vendor = \"rainxchzed\"\n            includeAllModules = true\n\n            val currentOs =\n                org.gradle.internal.os.OperatingSystem\n                    .current()\n            targetFormats(\n                *when {\n                    currentOs.isWindows -> arrayOf(TargetFormat.Exe, TargetFormat.Msi)\n                    currentOs.isMacOsX -> arrayOf(TargetFormat.Dmg, TargetFormat.Pkg)\n                    currentOs.isLinux -> arrayOf(TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.AppImage)\n                    else -> emptyArray()\n                },\n            )\n            windows {\n                iconFile.set(project.file(\"src/jvmMain/resources/logo/app_icon.ico\"))\n                menuGroup = \"Github Store\"\n                shortcut = true\n                perUserInstall = true\n            }\n            macOS {\n                iconFile.set(project.file(\"src/jvmMain/resources/logo/app_icon.icns\"))\n                bundleID = \"zed.rainxch.githubstore\"\n\n                infoPlist {\n                    extraKeysRawXml =\n                        \"\"\"\n                        <key>CFBundleURLTypes</key>\n                        <array>\n                            <dict>\n                                <key>CFBundleURLName</key>\n                                <string>GitHub Store Deep Link</string>\n                                <key>CFBundleURLSchemes</key>\n                                <array>\n                                    <string>githubstore</string>\n                                </array>\n                            </dict>\n                        </array>\n                        \"\"\".trimIndent()\n                }\n            }\n            linux {\n                iconFile.set(project.file(\"src/jvmMain/resources/logo/app_icon.png\"))\n                appRelease =\n                    libs.versions.projectVersionName\n                        .get()\n                        .toString()\n                debPackageVersion =\n                    libs.versions.projectVersionName\n                        .get()\n                        .toString()\n                menuGroup = \"Development\"\n                appCategory = \"Development\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/proguard-rules.pro",
    "content": "# ============================================================================\n# ProGuard / R8 Rules for GitHub Store (KMP + Compose Multiplatform)\n# ============================================================================\n# Used with: proguard-android-optimize.txt (enables optimization passes)\n# ============================================================================\n\n# ── General Attributes ──────────────────────────────────────────────────────\n-keepattributes Signature\n-keepattributes *Annotation*\n-keepattributes InnerClasses,EnclosingMethod\n-keepattributes SourceFile,LineNumberTable\n-keepattributes Exceptions\n\n# ── Kotlin Core ─────────────────────────────────────────────────────────────\n# Keep Kotlin metadata for reflection used by serialization & Koin\n-keep class kotlin.Metadata { *; }\n-keep class kotlin.reflect.jvm.internal.** { *; }\n-dontwarn kotlin.**\n-dontwarn kotlinx.**\n\n# ── Kotlin Coroutines ──────────────────────────────────────────────────────\n-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}\n-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}\n-keepclassmembernames class kotlinx.** { volatile <fields>; }\n-dontwarn kotlinx.coroutines.**\n\n# ── Kotlinx Serialization ──────────────────────────────────────────────────\n# Serialization engine internals\n-keep class kotlinx.serialization.** { *; }\n-keepclassmembers class kotlinx.serialization.json.** { *** Companion; }\n-dontnote kotlinx.serialization.**\n\n# Generated serializers for ALL @Serializable classes\n-keep class **$$serializer { *; }\n-keepclassmembers @kotlinx.serialization.Serializable class ** {\n    *** Companion;\n    *** INSTANCE;\n    kotlinx.serialization.KSerializer serializer(...);\n}\n\n# App @Serializable classes (DTOs, models, navigation routes) across all packages\n-keep @kotlinx.serialization.Serializable class zed.rainxch.** { *; }\n-keep,includedescriptorclasses class zed.rainxch.**$$serializer { *; }\n-keepclassmembers @kotlinx.serialization.Serializable class zed.rainxch.** {\n    *** Companion;\n}\n\n# ── Navigation Routes ──────────────────────────────────────────────────────\n# Type-safe navigation requires these classes to survive R8\n-keep class zed.rainxch.githubstore.app.navigation.GithubStoreGraph { *; }\n-keep class zed.rainxch.githubstore.app.navigation.GithubStoreGraph$* { *; }\n\n# ── Network DTOs – Core Module ─────────────────────────────────────────────\n-keep class zed.rainxch.core.data.dto.** { *; }\n\n# ── Network DTOs – Feature Modules ─────────────────────────────────────────\n-keep class zed.rainxch.search.data.dto.** { *; }\n-keep class zed.rainxch.devprofile.data.dto.** { *; }\n-keep class zed.rainxch.home.data.dto.** { *; }\n\n# ── Domain Models ──────────────────────────────────────────────────────────\n-keep class zed.rainxch.core.domain.model.GithubRepoSummary { *; }\n-keep class zed.rainxch.core.domain.model.GithubUser { *; }\n\n# Keep enums used by Room TypeConverters and serialization\n-keep class zed.rainxch.core.domain.model.InstallSource { *; }\n-keep class zed.rainxch.core.domain.model.AppTheme { *; }\n-keep class zed.rainxch.core.domain.model.FontTheme { *; }\n-keep class zed.rainxch.core.domain.model.Platform { *; }\n-keep class zed.rainxch.core.domain.model.SystemArchitecture { *; }\n-keep class zed.rainxch.core.domain.model.PackageChangeType { *; }\n\n# ── Room Database ──────────────────────────────────────────────────────────\n# Database class and generated implementation\n-keep class zed.rainxch.core.data.local.db.AppDatabase { *; }\n-keep class zed.rainxch.core.data.local.db.AppDatabase_Impl { *; }\n\n# Entities\n-keep class zed.rainxch.core.data.local.db.entities.** { *; }\n\n# DAOs\n-keep interface zed.rainxch.core.data.local.db.dao.** { *; }\n-keep class zed.rainxch.core.data.local.db.dao.** { *; }\n\n# Room runtime\n-keep class androidx.room.** { *; }\n-dontwarn androidx.room.**\n\n# ── Ktor ───────────────────────────────────────────────────────────────────\n# Engine discovery, plugin system, and content negotiation use reflection\n-keep class io.ktor.client.engine.** { *; }\n-keep class io.ktor.client.plugins.** { *; }\n-keep class io.ktor.serialization.** { *; }\n-keep class io.ktor.utils.io.** { *; }\n-keep class io.ktor.http.** { *; }\n-keepnames class io.ktor.** { *; }\n-dontwarn io.ktor.**\n-dontwarn java.lang.management.**\n\n# ── OkHttp (Ktor engine) ──────────────────────────────────────────────────\n-keep class okhttp3.internal.platform.** { *; }\n-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase\n-dontwarn okhttp3.**\n-dontwarn org.bouncycastle.**\n-dontwarn org.openjsse.**\n\n# ── Okio ───────────────────────────────────────────────────────────────────\n-dontwarn okio.**\n\n# ── SSL/TLS ────────────────────────────────────────────────────────────────\n-keep class org.conscrypt.** { *; }\n-dontwarn org.conscrypt.**\n\n# ── Koin DI ────────────────────────────────────────────────────────────────\n# Koin uses reflection for constructor injection\n-keep class org.koin.** { *; }\n-keep interface org.koin.** { *; }\n-dontwarn org.koin.**\n\n# Keep ViewModels so Koin can instantiate them\n-keep class zed.rainxch.**.presentation.**ViewModel { *; }\n-keep class zed.rainxch.**.presentation.**ViewModel$* { *; }\n\n# ── Compose / AndroidX ────────────────────────────────────────────────────\n# Compose runtime and navigation (most rules come bundled with the library)\n-dontwarn androidx.compose.**\n-dontwarn androidx.lifecycle.**\n\n# ── DataStore ──────────────────────────────────────────────────────────────\n-keep class androidx.datastore.** { *; }\n-keepclassmembers class androidx.datastore.preferences.** { *; }\n-dontwarn androidx.datastore.**\n\n# ── Landscapist / Coil3 (Image Loading) ────────────────────────────────────\n-keep class com.skydoves.landscapist.** { *; }\n-keep interface com.skydoves.landscapist.** { *; }\n-keep class coil3.** { *; }\n-dontwarn coil3.**\n-dontwarn com.skydoves.landscapist.**\n\n# ── Multiplatform Markdown Renderer ────────────────────────────────────────\n-keep class com.mikepenz.markdown.** { *; }\n-keep class org.intellij.markdown.** { *; }\n-dontwarn com.mikepenz.markdown.**\n-dontwarn org.intellij.markdown.**\n\n# ── Kermit Logging ─────────────────────────────────────────────────────────\n-keep class co.touchlab.kermit.** { *; }\n-dontwarn co.touchlab.kermit.**\n\n# ── MOKO Permissions ──────────────────────────────────────────────────────\n-keep class dev.icerock.moko.permissions.** { *; }\n-dontwarn dev.icerock.moko.**\n\n# ── BuildKonfig (Generated Build Constants) ────────────────────────────────\n-keep class zed.rainxch.githubstore.BuildConfig { *; }\n-keep class zed.rainxch.**.BuildKonfig { *; }\n-keep class **.BuildKonfig { *; }\n\n# ── AndroidX Security / Crypto ─────────────────────────────────────────────\n-keep class androidx.security.crypto.** { *; }\n-keep class com.google.crypto.tink.** { *; }\n-dontwarn com.google.crypto.tink.**\n-dontwarn com.google.errorprone.annotations.**\n\n# ── Firebase (if integrated) ──────────────────────────────────────────────\n-keep class com.google.firebase.** { *; }\n-dontwarn com.google.firebase.**\n\n# ── Enum safety ────────────────────────────────────────────────────────────\n# Keep all enum values and valueOf methods (used by serialization/Room)\n-keepclassmembers enum * {\n    public static **[] values();\n    public static ** valueOf(java.lang.String);\n}\n\n# ── Parcelable ─────────────────────────────────────────────────────────────\n-keepclassmembers class * implements android.os.Parcelable {\n    public static final ** CREATOR;\n}\n\n# ── Java Serializable Compatibility ───────────────────────────────────────\n-keepnames class * implements java.io.Serializable\n-keepclassmembers class * implements java.io.Serializable {\n    static final long serialVersionUID;\n    private static final java.io.ObjectStreamField[] serialPersistentFields;\n    !static !transient <fields>;\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n    java.lang.Object writeReplace();\n    java.lang.Object readResolve();\n}\n\n# ── Suppress Warnings for Missing Classes ──────────────────────────────────\n-dontwarn java.lang.invoke.StringConcatFactory\n-dontwarn javax.annotation.**\n-dontwarn org.slf4j.**\n-dontwarn org.codehaus.mojo.animal_sniffer.**\n"
  },
  {
    "path": "composeApp/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n    <uses-permission\n        android:name=\"android.permission.QUERY_ALL_PACKAGES\"\n        tools:ignore=\"PackageVisibilityPolicy,QueryAllPackagesPermission\" />\n\n    <uses-permission\n        android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"\n        tools:ignore=\"RequestInstallPackagesPolicy\" />\n    <uses-permission android:name=\"android.permission.REQUEST_DELETE_PACKAGES\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\" />\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n\n    <application\n        android:name=\".app.GithubStoreApp\"\n        android:allowBackup=\"true\"\n        android:hasFragileUserData=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.GitHubStore.Splash\"\n        android:usesCleartextTraffic=\"false\"\n        tools:targetApi=\"29\">\n\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTask\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"callback\"\n                    android:scheme=\"githubstore\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"repo\"\n                    android:scheme=\"githubstore\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.SEND\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"text/plain\" />\n                <data android:mimeType=\"text/html\" />\n            </intent-filter>\n\n            <intent-filter android:autoVerify=\"false\">\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"https\" />\n                <data android:host=\"github.com\" />\n                <data android:pathPattern=\"/.*/.*\" />\n            </intent-filter>\n\n            <intent-filter android:autoVerify=\"true\">\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"http\" />\n                <data android:scheme=\"https\" />\n                <data android:host=\"github-store.org\" />\n                <data android:pathPrefix=\"/app/\" />\n\n            </intent-filter>\n        </activity>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/filepaths\" />\n        </provider>\n\n        <!-- Reschedule update checks after device reboot -->\n        <receiver\n            android:name=\"zed.rainxch.core.data.services.BootReceiver\"\n            android:exported=\"false\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n            </intent-filter>\n        </receiver>\n\n        <!-- Static receiver for package install/remove events (works even when process is dead) -->\n        <receiver\n            android:name=\"zed.rainxch.core.data.services.PackageEventReceiver\"\n            android:exported=\"false\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.PACKAGE_ADDED\" />\n                <action android:name=\"android.intent.action.PACKAGE_REPLACED\" />\n                <action android:name=\"android.intent.action.PACKAGE_FULLY_REMOVED\" />\n                <data android:scheme=\"package\" />\n            </intent-filter>\n        </receiver>\n\n        <!-- WorkManager foreground service declaration for update workers -->\n        <service\n            android:name=\"androidx.work.impl.foreground.SystemForegroundService\"\n            android:foregroundServiceType=\"dataSync\"\n            tools:node=\"merge\" />\n\n        <!-- Shizuku provider for optional silent install support -->\n        <provider\n            android:name=\"rikka.shizuku.ShizukuProvider\"\n            android:authorities=\"${applicationId}.shizuku\"\n            android:multiprocess=\"false\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            android:permission=\"android.permission.INTERACT_ACROSS_USERS_FULL\" />\n    </application>\n\n</manifest>"
  },
  {
    "path": "composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt",
    "content": "package zed.rainxch.githubstore\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen\nimport androidx.core.util.Consumer\nimport org.koin.android.ext.android.inject\nimport zed.rainxch.core.data.utils.AndroidShareManager\nimport zed.rainxch.core.domain.utils.ShareManager\nimport zed.rainxch.githubstore.app.deeplink.DeepLinkParser\n\nclass MainActivity : ComponentActivity() {\n    private var deepLinkUri by mutableStateOf<String?>(null)\n    private val shareManager: ShareManager by inject()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        installSplashScreen()\n        enableEdgeToEdge()\n\n        // Register activity result launcher for file picker (must be before STARTED)\n        (shareManager as? AndroidShareManager)?.registerActivityResultLauncher(this)\n\n        super.onCreate(savedInstanceState)\n\n        handleIncomingIntent(intent)\n\n        setContent {\n            DisposableEffect(Unit) {\n                val listener =\n                    Consumer<Intent> { newIntent ->\n                        handleIncomingIntent(newIntent)\n                    }\n                addOnNewIntentListener(listener)\n                onDispose {\n                    removeOnNewIntentListener(listener)\n                }\n            }\n\n            App(deepLinkUri = deepLinkUri)\n        }\n    }\n\n    private fun handleIncomingIntent(intent: Intent?) {\n        if (intent == null) return\n\n        val uriString =\n            when (intent.action) {\n                Intent.ACTION_VIEW -> {\n                    intent.data?.toString()\n                }\n\n                Intent.ACTION_SEND -> {\n                    val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)\n                    sharedText?.let { DeepLinkParser.extractSupportedUrl(it) }\n                }\n\n                else -> {\n                    null\n                }\n            }\n\n        uriString?.let { deepLinkUri = it }\n    }\n}\n\n@Preview\n@Composable\nfun AppAndroidPreview() {\n    App()\n}\n"
  },
  {
    "path": "composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt",
    "content": "package zed.rainxch.githubstore.app\n\nimport android.app.Application\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.os.Build\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.get\nimport org.koin.android.ext.koin.androidContext\nimport zed.rainxch.core.data.services.PackageEventReceiver\nimport zed.rainxch.core.data.services.UpdateScheduler\nimport zed.rainxch.core.domain.model.InstallSource\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.PackageMonitor\nimport zed.rainxch.githubstore.app.di.initKoin\n\nclass GithubStoreApp : Application() {\n    private var packageEventReceiver: PackageEventReceiver? = null\n    private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)\n\n    override fun onCreate() {\n        super.onCreate()\n\n        initKoin {\n            androidContext(this@GithubStoreApp)\n        }\n\n        createNotificationChannels()\n        registerPackageEventReceiver()\n        scheduleBackgroundUpdateChecks()\n        registerSelfAsInstalledApp()\n    }\n\n    private fun createNotificationChannels() {\n        val notificationManager = getSystemService(NotificationManager::class.java)\n\n        val updatesChannel =\n            NotificationChannel(\n                UPDATES_CHANNEL_ID,\n                \"App Updates\",\n                NotificationManager.IMPORTANCE_HIGH,\n            ).apply {\n                description = \"Notifications when app updates are available\"\n            }\n        notificationManager.createNotificationChannel(updatesChannel)\n\n        val serviceChannel =\n            NotificationChannel(\n                UPDATE_SERVICE_CHANNEL_ID,\n                \"Update Service\",\n                NotificationManager.IMPORTANCE_LOW,\n            ).apply {\n                description = \"Background update check and auto-update progress\"\n                setShowBadge(false)\n            }\n        notificationManager.createNotificationChannel(serviceChannel)\n    }\n\n    private fun registerPackageEventReceiver() {\n        val receiver =\n            PackageEventReceiver(\n                installedAppsRepository = get<InstalledAppsRepository>(),\n                packageMonitor = get<PackageMonitor>(),\n            )\n        val filter = PackageEventReceiver.createIntentFilter()\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)\n        } else {\n            registerReceiver(receiver, filter)\n        }\n\n        packageEventReceiver = receiver\n    }\n\n    private fun scheduleBackgroundUpdateChecks() {\n        appScope.launch {\n            try {\n                val intervalHours = get<TweaksRepository>().getUpdateCheckInterval().first()\n                UpdateScheduler.schedule(\n                    context = this@GithubStoreApp,\n                    intervalHours = intervalHours,\n                )\n            } catch (e: Exception) {\n                Logger.e(e) { \"Failed to schedule background update checks\" }\n            }\n        }\n    }\n\n    private fun registerSelfAsInstalledApp() {\n        appScope.launch {\n            try {\n                val repo = get<InstalledAppsRepository>()\n                val selfPackageName = packageName\n                val existing = repo.getAppByPackage(selfPackageName)\n\n                if (existing != null) return@launch\n\n                val packageMonitor = get<PackageMonitor>()\n                val systemInfo = packageMonitor.getInstalledPackageInfo(selfPackageName)\n                if (systemInfo == null) {\n                    Logger.w { \"GithubStoreApp: Skip self-registration, package info missing for $selfPackageName\" }\n                    return@launch\n                }\n\n                val now = System.currentTimeMillis()\n                val versionName = systemInfo.versionName\n                val versionCode = systemInfo.versionCode\n\n                val selfApp =\n                    InstalledApp(\n                        packageName = selfPackageName,\n                        repoId = SELF_REPO_ID,\n                        repoName = SELF_REPO_NAME,\n                        repoOwner = SELF_REPO_OWNER,\n                        repoOwnerAvatarUrl = SELF_AVATAR_URL,\n                        repoDescription = \"A cross-platform app store for GitHub releases\",\n                        primaryLanguage = \"Kotlin\",\n                        repoUrl = \"https://github.com/$SELF_REPO_OWNER/$SELF_REPO_NAME\",\n                        installedVersion = versionName,\n                        installedAssetName = null,\n                        installedAssetUrl = null,\n                        latestVersion = null,\n                        latestAssetName = null,\n                        latestAssetUrl = null,\n                        latestAssetSize = null,\n                        appName = \"GitHub Store\",\n                        installSource = InstallSource.THIS_APP,\n                        installedAt = now,\n                        lastCheckedAt = 0L,\n                        lastUpdatedAt = now,\n                        isUpdateAvailable = false,\n                        updateCheckEnabled = true,\n                        releaseNotes = null,\n                        systemArchitecture = \"\",\n                        fileExtension = \"apk\",\n                        isPendingInstall = false,\n                        installedVersionName = versionName,\n                        installedVersionCode = versionCode,\n                        signingFingerprint = SELF_SHA256_FINGERPRINT,\n                    )\n\n                repo.saveInstalledApp(selfApp)\n                Logger.i(\"GitHub Store App: App added\")\n            } catch (e: Exception) {\n                Logger.e(e) { \"GitHub Store App: Failed to register self as installed app\" }\n            }\n        }\n    }\n\n    companion object {\n        private const val SELF_REPO_ID = 1101281251L\n        private const val SELF_SHA256_FINGERPRINT =\n            @Suppress(\"ktlint:standard:max-line-length\")\n            \"B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA\"\n        private const val SELF_REPO_OWNER = \"OpenHub-Store\"\n        private const val SELF_REPO_NAME = \"GitHub-Store\"\n        private const val SELF_AVATAR_URL =\n            @Suppress(\"ktlint:standard:max-line-length\")\n            \"https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/refs/heads/main/media-resources/app_icon.png\"\n        const val UPDATES_CHANNEL_ID = \"app_updates\"\n        const val UPDATE_SERVICE_CHANNEL_ID = \"update_service\"\n    }\n}\n"
  },
  {
    "path": "composeApp/src/androidMain/res/drawable/ic_launcher_monochrome.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1800\"\n    android:viewportHeight=\"1800\">\n\n    <group\n        android:translateX=\"0\"\n        android:translateY=\"1800\"\n        android:scaleX=\"0.1\"\n        android:scaleY=\"-0.1\">\n\n        <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M5100 14990 c-72 -8 -179 -32 -223 -49 -16 -6 -39 -11 -51 -11 -13 0\n-29 -7 -36 -15 -7 -8 -19 -15 -27 -15 -7 0 -51 -18 -95 -40 -45 -22 -86 -40\n-91 -40 -15 0 -205 -141 -211 -156 -3 -8 -11 -14 -19 -14 -19 0 -267 -264\n-267 -284 0 -4 -15 -28 -34 -55 -72 -102 -136 -258 -186 -456 l-34 -130 0\n-4720 -1 -4720 13 -45 c8 -25 18 -70 23 -100 6 -30 14 -64 18 -75 5 -11 16\n-40 26 -65 9 -25 25 -62 36 -82 10 -20 19 -44 19 -52 0 -17 35 -78 119 -206\nl55 -85 97 -95 c53 -52 118 -110 144 -129 79 -56 194 -131 201 -131 3 0 38\n-15 77 -34 141 -68 272 -101 459 -117 l143 -11 85 6 c47 4 121 11 165 15 l80\n8 105 33 105 33 145 75 c80 41 154 82 165 92 26 24 209 120 304 160 l74 32 24\n-13 c28 -15 30 -29 4 -29 -11 0 -22 -4 -26 -9 -6 -10 9 -16 66 -25 l36 -6 23\n-25 c13 -14 32 -25 42 -25 l18 0 0 15 0 16 48 -1 c60 -2 108 8 123 26 6 8 20\n14 31 14 10 0 21 7 24 15 4 8 14 15 23 15 26 0 121 48 127 65 3 8 35 24 70 35\n56 19 84 40 54 40 -5 0 -10 5 -10 11 l0 11 25 -6 25 -6 50 25 c27 14 50 29 50\n34 0 5 24 21 53 35 30 14 59 32 66 40 7 9 50 34 95 55 44 22 92 47 106 55 14\n8 51 27 83 40 31 14 57 30 57 34 0 5 12 15 28 22 15 8 39 25 54 39 24 21 39\n27 84 30 18 1 104 75 104 88 0 7 14 19 30 28 17 9 30 20 30 25 0 5 10 12 23\n15 12 4 31 15 42 26 11 10 31 28 43 39 13 12 43 29 67 37 24 8 49 21 56 29 6\n8 22 14 35 14 13 0 27 7 32 17 l10 16 33 -1 c18 0 57 10 86 24 30 13 60 24 67\n24 8 0 20 10 28 23 9 12 47 37 86 56 l71 35 -10 13 -11 13 16 6 c9 4 33 4 52\n1 l36 -6 -12 14 c-7 8 -10 22 -6 30 7 20 36 19 36 -2 0 -9 3 -13 7 -9 5 4 8\n13 8 19 0 6 11 17 24 23 13 7 49 37 80 68 31 31 64 56 73 56 9 0 26 14 38 30\nl21 30 55 25 55 25 19 45 19 45 21 0 c12 0 31 11 43 24 l22 23 70 7 c39 4 80\n7 92 7 12 -1 26 7 31 15 9 16 20 19 122 31 30 3 62 11 70 18 8 8 29 15 45 17\nl30 3 0 15 0 15 96 47 96 46 14 31 14 31 16 0 c9 0 20 7 23 16 l6 15 11 -6\nc12 -8 59 32 59 49 0 6 8 16 19 23 l18 11 7 44 7 43 20 16 21 15 -22 38 -21\n39 15 -7 c9 -3 23 -9 31 -12 l15 -6 0 18 c0 9 -7 25 -16 35 l-15 17 12 20 c7\n11 23 25 36 31 13 6 51 32 86 58 66 52 270 179 365 228 31 17 101 56 154 88\n53 31 101 57 106 57 5 0 17 9 27 20 10 11 23 20 30 20 9 0 179 94 287 159 89\n54 283 165 308 177 8 4 76 43 150 87 74 44 151 89 170 100 19 11 80 45 135 76\n55 32 146 83 203 115 56 32 105 62 108 67 3 5 11 9 19 9 8 0 34 14 58 30 25\n17 48 30 52 30 5 0 51 26 104 59 53 32 119 71 146 86 123 67 302 169 327 186\n15 11 31 19 36 19 5 0 17 9 27 20 10 11 24 20 31 20 7 0 36 13 64 30 28 16 53\n30 56 30 7 0 195 110 214 126 8 6 38 24 65 38 28 15 77 43 110 62 33 19 83 46\n110 60 181 96 505 379 505 442 0 6 8 17 18 24 15 11 92 130 92 143 0 2 13 26\n28 52 16 26 31 57 35 68 3 11 24 74 46 140 23 66 41 133 41 148 0 16 5 38 11\n49 6 11 15 83 20 159 l9 139 -9 143 -10 142 -21 88 c-11 49 -29 110 -39 135\n-10 26 -28 70 -40 98 -11 28 -45 95 -76 149 -30 54 -55 101 -55 105 0 4 -13\n19 -30 33 -16 15 -30 34 -30 42 0 45 -396 382 -452 385 -4 0 -35 17 -70 37\n-35 21 -72 43 -83 50 -11 6 -39 21 -62 32 -24 11 -67 36 -96 55 -30 20 -56 36\n-59 36 -3 0 -38 18 -77 39 -39 22 -81 43 -92 46 -11 4 -46 24 -77 44 -31 21\n-89 54 -128 74 -61 30 -170 92 -224 127 -36 24 -110 60 -121 60 -7 0 -20 6\n-28 14 -9 7 -36 25 -61 38 -56 32 -200 115 -220 128 -8 6 -25 14 -37 19 -11 6\n-24 14 -27 19 -3 6 -17 13 -31 16 -13 3 -44 20 -69 36 -25 17 -65 39 -89 50\n-24 11 -49 25 -55 32 -6 7 -50 33 -99 58 -48 25 -89 48 -91 53 -2 4 -8 7 -13\n7 -8 0 -100 51 -244 137 -27 16 -75 41 -105 56 -30 15 -73 37 -95 50 -87 50\n-101 57 -125 67 -14 6 -37 18 -51 28 -14 9 -50 32 -80 50 -30 19 -85 56 -124\n83 -38 27 -86 61 -105 74 -19 13 -46 33 -60 44 l-25 20 17 6 c9 4 22 4 28 0 5\n-3 15 -3 21 0 l10 7 -20 15 c-12 8 -21 20 -21 26 0 7 -9 18 -21 25 l-20 13 6\n30 7 29 -70 70 -70 70 -29 0 -29 0 -17 20 c-22 26 -28 25 -61 -5 -15 -14 -31\n-25 -37 -25 -17 0 -9 18 18 40 l26 21 -11 11 -12 12 -18 -17 c-10 -9 -28 -17\n-40 -17 l-22 0 0 -15 c0 -16 -33 -27 -69 -23 l-24 3 -4 -12 -5 -13 41 0 41 0\n0 -10 c0 -5 -9 -10 -20 -10 l-20 0 0 -14 c0 -8 12 -17 28 -21 15 -3 38 -17 52\n-30 29 -26 198 -125 216 -125 6 0 18 -8 26 -17 l16 -18 6 -95 c4 -52 14 -144\n23 -205 l16 -110 -1 -325 -1 -325 -11 -88 -11 -89 18 -29 c10 -16 41 -66 70\n-111 68 -111 122 -217 190 -373 107 -248 130 -320 208 -640 23 -94 45 -235 45\n-288 l0 -48 15 -5 c8 -4 80 10 159 30 176 44 174 44 215 2 l32 -33 -7 -38 -7\n-37 -31 -19 c-17 -10 -47 -21 -66 -24 -116 -17 -272 -50 -279 -59 -5 -6 -11\n-63 -15 -127 l-7 -117 16 -16 16 -16 42 0 c66 0 286 -20 351 -32 l58 -11 24\n-26 24 -26 0 -28 c0 -15 -10 -41 -22 -57 l-22 -30 -65 0 c-36 0 -97 7 -135 15\n-39 8 -119 20 -180 26 l-110 12 -18 -23 c-10 -12 -18 -31 -18 -41 0 -10 -11\n-47 -24 -83 l-23 -65 16 -15 c9 -8 30 -18 46 -22 17 -4 52 -18 79 -30 28 -13\n54 -24 59 -24 21 0 240 -105 260 -124 l17 -17 0 -35 0 -35 -25 -24 -24 -25\n-30 0 c-29 0 -94 22 -171 57 -67 30 -225 104 -257 120 l-32 16 -15 -12 c-8 -7\n-36 -44 -62 -83 -108 -158 -332 -352 -582 -501 -260 -155 -706 -345 -873 -372\n-20 -3 -43 -13 -52 -23 l-16 -17 23 -65 c13 -36 40 -110 60 -165 48 -133 51\n-142 92 -270 19 -60 39 -123 45 -140 11 -31 35 -108 49 -160 4 -16 18 -66 30\n-110 13 -44 26 -98 31 -120 7 -37 31 -140 65 -280 38 -153 88 -459 80 -480 -2\n-7 -58 -39 -123 -71 -65 -32 -145 -74 -178 -94 -33 -20 -68 -39 -77 -43 -10\n-3 -48 -26 -85 -50 -37 -24 -108 -70 -158 -102 -49 -31 -102 -65 -116 -75 -14\n-10 -37 -20 -50 -22 l-24 -3 1 39 c1 21 9 63 17 95 37 135 50 203 54 306 l5\n110 -20 25 -20 25 -28 -10 -29 -10 -14 -29 c-7 -15 -18 -58 -23 -95 -5 -36\n-13 -88 -18 -116 -5 -27 -16 -95 -25 -150 -9 -55 -23 -125 -30 -155 -8 -30\n-17 -67 -20 -82 -6 -31 -51 -88 -70 -88 -24 0 -45 43 -105 220 -5 14 -18 50\n-30 80 -12 30 -25 66 -29 80 -5 14 -17 45 -28 70 -11 25 -28 65 -38 90 -18 44\n-77 168 -103 216 -16 29 -55 40 -88 25 l-24 -11 0 -28 c0 -34 27 -129 44 -157\n7 -11 30 -63 50 -115 20 -52 43 -108 51 -125 7 -16 21 -50 30 -75 10 -25 24\n-58 31 -75 36 -77 97 -233 101 -258 l5 -27 -14 0 c-14 0 -160 -72 -239 -118\n-19 -11 -65 -37 -104 -57 -38 -21 -77 -43 -85 -50 -8 -7 -35 -23 -60 -35 -53\n-26 -182 -109 -193 -125 l-8 -10 -58 62 c-31 34 -76 89 -99 123 -24 34 -47 67\n-53 73 -15 18 -139 208 -139 214 0 2 -15 28 -34 56 -18 29 -61 104 -94 167\n-33 63 -70 125 -82 138 l-22 23 -26 -3 -26 -3 -15 -27 -16 -27 19 -58 c17 -50\n56 -132 116 -238 98 -174 126 -220 172 -285 29 -41 87 -116 130 -166 42 -50\n78 -99 80 -108 l3 -18 -35 -19 c-19 -10 -73 -37 -120 -59 -47 -22 -103 -50\n-125 -63 -22 -12 -71 -40 -110 -62 -38 -21 -108 -62 -155 -90 -47 -29 -114\n-68 -150 -88 -36 -19 -88 -51 -115 -69 -28 -19 -78 -48 -112 -66 -35 -18 -119\n-66 -188 -107 -70 -41 -129 -75 -131 -75 -3 0 -35 -20 -72 -44 -37 -24 -105\n-67 -152 -95 -47 -28 -96 -60 -110 -72 l-26 -20 -16 6 c-9 4 -21 19 -27 33 -6\n15 -27 54 -46 87 -19 33 -53 96 -76 140 -22 44 -50 96 -61 115 -37 64 -57 104\n-98 195 -23 50 -50 108 -60 130 -10 22 -26 62 -36 88 -23 63 -8 60 -243 61\nl-206 1 -100 33 c-55 19 -113 40 -130 47 -16 8 -61 28 -100 46 -297 134 -515\n428 -560 754 -3 22 -9 90 -15 150 l-10 110 9 177 9 176 -9 114 c-20 274 -89\n415 -258 530 l-73 50 -25 55 -26 55 5 62 c6 64 23 107 64 163 l25 32 62 31 62\n30 67 0 67 0 73 -34 74 -33 53 -47 c271 -241 313 -385 312 -1066 l-1 -345 14\n-75 c58 -312 226 -467 557 -515 59 -8 123 -15 142 -15 l36 0 11 19 c5 11 10\n50 10 88 0 78 29 250 76 453 14 60 23 94 61 240 27 102 45 165 55 190 6 14 14\n41 18 60 13 59 38 141 75 245 20 55 47 132 60 170 51 147 97 271 111 300 9 17\n32 71 51 120 35 87 68 161 158 350 73 155 95 200 105 215 5 8 17 33 26 54 l18\n39 -23 18 c-13 11 -33 19 -46 19 -12 0 -35 4 -51 9 -16 5 -83 26 -149 46 -66\n20 -132 43 -147 50 -14 8 -31 15 -37 15 -9 0 -239 105 -284 131 -10 5 -46 24\n-80 41 -200 100 -412 248 -567 397 -103 99 -102 98 -120 76 -7 -8 -20 -15 -30\n-15 -10 0 -23 -7 -30 -15 -7 -8 -20 -15 -30 -15 -10 0 -23 -7 -30 -15 -6 -8\n-22 -17 -33 -21 -12 -4 -39 -17 -59 -30 -20 -13 -81 -47 -134 -75 l-98 -51\n-29 6 c-16 3 -37 16 -47 28 l-19 23 -1 36 0 35 33 31 32 30 160 77 c88 42 176\n86 195 96 l35 19 -2 25 c-1 13 -13 42 -27 64 -14 22 -28 52 -32 66 -3 15 -15\n33 -26 40 l-20 14 -37 -5 c-45 -6 -166 -28 -289 -54 -51 -10 -106 -19 -123\n-19 l-30 0 -29 22 -30 22 0 41 0 42 19 17 c32 29 134 51 351 77 52 7 101 17\n108 22 l14 11 -5 35 c-14 113 -31 207 -38 217 -8 10 -71 19 -262 40 l-58 6\n-32 22 -32 22 -3 46 -3 47 26 17 27 17 126 -13 c70 -7 138 -17 152 -22 13 -5\n35 -7 47 -3 l23 5 0 98 c0 141 34 386 77 547 25 94 73 257 85 285 5 14 21 59\n35 100 13 41 35 97 49 123 13 27 24 53 24 59 0 14 91 194 125 247 l25 41 0 42\nc-1 24 -5 63 -10 88 -5 25 -18 126 -30 224 l-20 179 0 296 1 296 14 80 c8 44\n15 96 15 115 0 19 9 80 20 135 12 55 27 132 34 170 46 238 150 509 216 562 16\n13 55 30 88 38 l60 16 38 -10 c79 -18 150 -40 189 -56 46 -20 311 -152 370\n-185 108 -60 177 -103 217 -133 24 -18 61 -44 83 -57 105 -63 391 -294 517\n-417 l98 -96 52 7 c29 4 78 13 108 21 303 74 840 87 1215 29 132 -21 287 -52\n316 -63 l24 -9 71 72 c38 39 113 109 165 156 51 47 96 88 99 91 37 42 502 369\n526 369 8 0 183 -88 199 -100 21 -16 128 -70 140 -70 7 0 22 -9 35 -20 34 -29\n73 -9 42 22 l-11 11 12 19 12 19 -34 64 c-19 36 -40 65 -47 65 -6 0 -14 7 -18\n15 -3 8 -12 15 -21 15 -8 0 -20 11 -25 25 -5 14 -16 25 -25 25 -8 0 -15 4 -15\n10 0 5 -22 22 -50 38 l-50 29 -70 -3 -71 -2 -18 -16 c-10 -9 -22 -16 -28 -16\n-5 0 -15 -11 -21 -25 -16 -34 -37 -41 -75 -22 -18 8 -75 36 -127 61 -52 25\n-102 50 -110 55 -8 5 -49 25 -90 45 -74 36 -120 62 -270 146 -71 40 -183 101\n-260 143 -19 10 -48 27 -65 37 -16 10 -61 35 -99 55 -38 21 -83 46 -100 56\n-17 10 -55 30 -83 44 -29 13 -53 28 -53 32 0 5 -24 19 -52 32 -29 13 -70 35\n-91 48 -20 13 -76 44 -125 69 -48 25 -93 51 -101 57 -16 14 -26 19 -154 84\n-89 46 -228 123 -402 224 -27 16 -88 50 -135 75 -47 26 -132 74 -190 107 -58\n33 -132 75 -165 92 -161 85 -263 140 -295 160 -19 12 -60 34 -90 49 -30 15\n-68 35 -85 45 -16 10 -61 35 -100 56 -38 21 -82 46 -97 56 -14 11 -30 19 -36\n19 -5 0 -20 8 -33 19 -13 10 -35 22 -48 26 -14 4 -52 24 -85 44 -34 21 -99 58\n-146 83 -47 25 -123 67 -170 92 -101 56 -199 93 -295 112 -38 7 -88 18 -110\n24 -52 13 -372 21 -455 10z m406 -135 c4 -8 17 -15 29 -15 13 0 27 -4 30 -10\n3 -5 23 -10 43 -10 20 0 50 -7 66 -15 15 -8 37 -15 47 -15 10 0 21 -4 24 -10\n3 -5 30 -21 58 -34 l52 -24 30 5 30 4 29 -30 29 -31 13 11 c17 14 36 2 32 -19\n-1 -10 1 -22 5 -29 l7 -12 31 11 31 11 28 -32 c16 -17 40 -35 54 -41 l26 -10\n0 -20 0 -20 17 0 c21 0 74 -17 144 -46 31 -13 63 -24 72 -24 8 0 35 -18 59\n-41 24 -22 50 -46 58 -53 20 -17 163 -86 178 -86 7 0 25 -8 40 -18 61 -42 109\n-70 140 -82 17 -7 32 -17 32 -22 0 -5 15 -16 33 -24 17 -8 39 -18 47 -23 8 -5\n36 -19 63 -30 l47 -21 0 -19 0 -20 21 -6 22 -7 9 16 c5 9 16 16 23 16 l15 0 0\n-21 0 -20 33 -14 c19 -8 39 -21 46 -29 7 -8 26 -18 42 -21 16 -4 29 -12 29\n-19 0 -7 9 -21 21 -32 l20 -18 18 11 c22 14 31 8 31 -19 0 -10 5 -18 11 -18 7\n0 22 -11 34 -24 l23 -25 62 -7 63 -7 16 -18 c8 -11 20 -19 26 -19 6 0 27 -13\n46 -29 20 -17 58 -38 85 -49 27 -10 69 -37 94 -60 25 -23 77 -58 115 -77 39\n-19 82 -46 95 -60 l24 -26 43 7 43 6 0 -16 0 -16 25 -6 c13 -3 29 -12 36 -20\n6 -8 20 -14 30 -14 10 0 26 -8 36 -19 10 -10 36 -28 59 -39 22 -11 46 -28 53\n-36 l13 -16 24 6 24 6 26 -16 25 -17 -6 -19 -6 -19 21 -11 c12 -6 44 -18 71\n-26 27 -8 49 -19 49 -24 0 -14 115 -70 144 -70 14 0 28 -5 32 -11 l7 -12 -17\n-9 c-9 -5 -16 -18 -16 -28 l0 -20 15 0 c9 0 18 7 21 15 l6 15 33 0 32 0 25\n-19 c13 -11 37 -23 51 -26 l27 -7 0 -19 0 -19 25 -6 c13 -3 29 -12 36 -20 l12\n-14 18 5 18 6 8 -14 c4 -7 26 -22 48 -34 l40 -20 3 -24 c5 -33 41 -69 68 -69\n13 0 25 -3 28 -8 8 -13 -18 -82 -32 -82 -7 0 -21 -13 -30 -29 -9 -17 -25 -32\n-34 -35 -10 -2 -21 -12 -25 -23 -5 -10 -25 -41 -46 -70 l-38 -52 -32 -7 c-18\n-3 -42 -18 -55 -31 l-23 -25 -20 5 c-20 5 -34 1 -108 -30 l-33 -15 -27 6 c-14\n4 -46 6 -69 6 -24 -1 -50 4 -58 11 l-15 12 -25 -13 -25 -14 -58 12 -57 12\n-272 0 c-149 0 -273 4 -276 8 -3 5 -28 9 -56 10 l-51 1 -5 -14 -6 -14 -102 6\n-103 6 -7 -12 -7 -11 -64 0 c-35 0 -67 5 -70 10 -3 6 -22 10 -41 10 l-36 0\n-61 29 c-34 15 -71 38 -81 50 -11 11 -25 21 -33 21 -7 0 -16 7 -20 16 -3 8\n-15 20 -27 25 -11 5 -55 34 -96 64 -41 30 -80 55 -87 55 -7 0 -20 14 -28 31\nl-16 30 -44 19 c-24 10 -48 27 -54 38 -6 11 -26 27 -46 36 -19 9 -46 28 -60\n41 -13 14 -33 25 -43 25 -10 0 -39 13 -63 30 -25 16 -85 46 -134 66 -50 20\n-99 45 -110 54 -34 29 -208 113 -250 121 -22 4 -69 15 -105 24 -36 9 -78 19\n-95 22 -16 3 -35 13 -41 20 l-10 15 -65 -6 c-35 -3 -82 -7 -104 -9 -59 -6\n-142 -54 -215 -125 -36 -35 -76 -70 -90 -77 -14 -8 -37 -29 -52 -47 -15 -18\n-41 -49 -57 -69 -17 -20 -43 -62 -57 -93 -14 -30 -30 -58 -35 -62 l-10 -6 6\n-29 7 -30 -12 -15 c-7 -8 -16 -31 -21 -52 -4 -20 -11 -50 -15 -67 -4 -16 -12\n-37 -19 -45 -12 -14 -29 -84 -30 -122 0 -9 -3 -20 -7 -24 -5 -4 -8 -26 -8 -49\n0 -22 -4 -45 -9 -51 -8 -8 -29 -172 -35 -279 -2 -22 -8 -68 -15 -102 l-12 -62\n7 -493 c8 -573 8 -573 -11 -601 -8 -12 -15 -34 -15 -49 l0 -27 -29 -33 c-16\n-18 -32 -48 -36 -66 -4 -18 -18 -52 -31 -75 -13 -23 -24 -51 -24 -62 0 -11\n-10 -38 -22 -60 -12 -22 -22 -49 -23 -60 -1 -11 -10 -40 -22 -65 -33 -71 -44\n-115 -49 -189 l-4 -68 -26 -46 c-14 -26 -23 -51 -19 -57 3 -5 -1 -20 -9 -32\nl-15 -22 6 -24 6 -25 -14 -11 c-8 -6 -23 -38 -33 -71 -11 -33 -25 -67 -32 -75\n-19 -25 -44 -78 -44 -96 0 -9 -20 -34 -45 -56 l-45 -40 0 -24 0 -24 -20 0 -20\n0 -11 -22 c-5 -13 -20 -38 -33 -56 l-24 -32 -7 -50 c-8 -60 1 -122 25 -170 40\n-78 29 -144 -50 -300 l-27 -55 2 -61 3 -62 18 -37 c9 -20 23 -39 31 -42 7 -3\n13 -11 13 -19 0 -7 10 -24 23 -38 79 -87 90 -107 67 -121 -6 -3 -10 -26 -10\n-50 l0 -44 20 -11 20 -11 0 -22 c0 -12 11 -39 25 -59 14 -20 25 -47 25 -58 l0\n-21 83 -80 c45 -43 89 -79 98 -79 8 0 24 -7 35 -15 l19 -14 97 -1 97 0 7 11 6\n11 30 -6 c16 -3 38 -2 47 4 l18 9 27 -14 28 -15 22 6 22 5 70 -37 c38 -20 76\n-43 85 -50 8 -8 30 -14 47 -14 l31 0 6 -21 c4 -11 18 -24 31 -29 13 -5 24 -14\n24 -20 0 -5 34 -26 75 -45 41 -19 75 -39 75 -45 0 -11 134 -80 156 -80 7 0 25\n-5 39 -12 l25 -11 0 -19 0 -18 25 0 c13 0 59 -20 101 -45 41 -25 84 -45 93\n-45 10 0 26 -10 37 -21 10 -12 35 -32 54 -44 l36 -23 11 -37 12 -37 -10 -25\nc-5 -13 -9 -39 -9 -56 0 -18 -7 -45 -15 -61 -8 -15 -15 -36 -15 -45 0 -9 -13\n-41 -30 -71 -16 -30 -30 -64 -30 -75 0 -11 -14 -47 -30 -80 -17 -33 -30 -70\n-30 -82 l0 -22 -20 -11 -20 -11 0 -22 c0 -12 -11 -43 -25 -69 -14 -26 -25 -54\n-25 -63 0 -9 -9 -35 -20 -57 -11 -23 -20 -49 -20 -57 0 -9 -11 -38 -25 -64\nl-26 -47 16 -6 c8 -3 15 -12 15 -20 l0 -14 -20 0 -20 0 0 -21 c0 -12 -6 -35\n-14 -52 -7 -18 -21 -54 -31 -80 l-17 -48 11 -12 10 -13 -19 -29 c-11 -17 -17\n-37 -14 -47 2 -9 0 -19 -6 -23 -5 -3 -10 -15 -10 -27 0 -11 -6 -34 -14 -51\n-15 -35 -27 -82 -27 -109 0 -10 -4 -20 -10 -23 -5 -4 -9 -15 -9 -26 l0 -19\n-21 0 -21 0 5 -19 5 -20 -13 -5 c-7 -2 -16 -23 -20 -45 -8 -49 -15 -54 -72\n-54 l-44 1 -24 26 c-14 15 -25 36 -25 46 0 10 -5 22 -11 26 l-12 7 6 63 c2 34\n1 66 -4 71 -4 4 -10 114 -12 243 -3 129 -7 255 -10 280 -3 25 -8 83 -12 130\nl-6 85 -15 12 -16 12 6 19 7 20 -22 41 -21 42 5 34 6 35 -14 0 -13 0 -41 85\nc-22 47 -45 85 -51 85 -6 0 -20 21 -33 48 l-23 47 -28 -3 -28 -3 6 24 6 24\n-50 49 c-27 27 -66 66 -87 86 l-38 37 -48 12 -48 12 6 18 6 19 -28 0 c-15 0\n-37 7 -47 15 -11 8 -32 15 -46 15 -14 0 -31 5 -36 10 -6 6 -42 13 -80 16 -38\n3 -90 10 -115 15 l-47 10 -47 -16 c-69 -23 -85 -33 -137 -87 -25 -26 -51 -48\n-57 -48 -6 0 -16 -9 -23 -20 -7 -11 -17 -20 -22 -20 -15 0 -66 -52 -66 -67 0\n-7 -11 -18 -24 -23 -13 -5 -30 -22 -38 -37 l-13 -28 -18 0 c-9 0 -41 -26 -71\n-59 l-55 -59 -15 6 -16 6 6 29 6 30 -14 23 c-16 29 -16 52 0 100 l13 36 -11\n24 c-5 13 -14 31 -18 39 -5 8 -14 25 -20 37 l-12 22 10 6 10 6 0 149 0 149 -9\n5 -9 6 6 83 c3 45 7 142 9 215 l4 134 -9 11 -10 12 6 95 c3 52 7 146 9 208 l4\n113 -9 12 -10 12 3 140 c2 77 7 148 10 158 4 9 4 30 0 45 -3 15 -5 35 -4 45 4\n43 2 367 -3 412 l-5 50 10 13 10 13 -12 25 -11 25 9 17 9 17 -7 575 -7 575 9\n12 c10 14 11 498 1 514 -4 7 -8 48 -9 93 -1 45 -6 147 -10 228 l-8 148 13 15\n13 16 4 212 3 212 11 42 11 43 -7 22 c-19 57 -38 569 -22 585 11 10 8 61 -3\n68 l-10 6 0 123 0 122 11 11 10 10 -5 94 c-4 52 -9 148 -12 213 l-5 119 10 10\n11 11 0 153 0 154 14 27 14 27 -4 53 -3 52 9 0 c6 0 10 6 10 13 0 7 9 29 20\n50 11 20 20 44 20 52 0 8 4 15 9 15 4 0 20 23 35 51 l26 52 -5 18 -6 18 20 10\nc11 6 26 9 33 6 l13 -5 33 49 c35 54 38 67 20 75 l-13 5 23 17 c12 9 22 23 22\n30 0 8 4 14 9 14 5 0 11 9 14 20 l5 21 27 -7 27 -7 -6 21 c-9 27 10 62 32 62\n10 0 38 22 62 50 l45 50 20 0 c24 0 85 36 103 61 6 9 25 20 41 24 16 3 35 13\n42 21 6 8 16 14 22 14 17 0 87 33 87 41 0 12 66 49 88 49 11 0 33 7 48 15 16\n8 45 15 65 15 19 0 39 4 45 10 8 8 41 11 124 12 81 0 227 11 233 17 5 5 31 9\n58 10 l50 1 5 -15z m5824 -3235 l10 -20 45 2 44 2 18 -16 18 -16 32 5 32 5 58\n-32 c32 -18 68 -46 81 -61 l22 -27 41 -7 40 -7 9 -24 10 -24 32 0 c44 0 114\n-36 138 -71 l21 -29 75 -35 c41 -19 100 -47 132 -62 31 -14 63 -34 70 -44 7\n-11 18 -19 24 -19 7 0 21 -9 33 -20 12 -12 44 -26 71 -33 l49 -12 -3 -23 -3\n-22 15 -6 c9 -4 23 -1 31 6 l15 12 6 -15 c3 -9 19 -20 35 -26 16 -5 29 -14 29\n-20 0 -5 30 -24 68 -42 37 -18 72 -40 79 -48 l11 -16 37 4 38 3 -7 -25 -6 -24\n50 5 50 5 0 -12 c0 -6 -11 -13 -25 -17 -31 -8 -23 -24 11 -24 30 0 166 -63\n185 -86 7 -8 37 -27 68 -42 31 -15 62 -33 69 -39 7 -7 20 -13 27 -13 8 0 19\n-9 25 -21 l11 -20 42 3 42 3 3 -20 3 -20 102 -50 c93 -45 140 -74 220 -135\nl27 -22 6 16 c3 9 15 16 26 16 l19 0 -2 -17 c-3 -33 1 -43 16 -43 8 0 24 -14\n37 -30 21 -30 69 -60 93 -60 6 0 29 -18 50 -40 21 -22 45 -40 52 -40 8 0 29\n-16 49 -35 19 -19 46 -42 59 -51 14 -9 25 -20 25 -25 0 -14 61 -99 71 -99 6 0\n17 -16 25 -35 8 -19 25 -42 38 -50 l23 -15 54 -108 c29 -59 52 -111 50 -116\n-1 -5 12 -38 29 -73 l31 -63 -6 -55 -7 -55 17 -26 16 -25 -5 -92 c-6 -91 -6\n-114 -4 -275 l1 -83 -11 -11 -12 -12 -15 20 c-8 10 -15 14 -15 9 0 -6 5 -16\n11 -22 l12 -12 -5 -58 -5 -58 -51 -106 c-29 -58 -52 -111 -52 -117 0 -7 -9\n-21 -19 -31 -11 -11 -23 -36 -26 -55 l-7 -35 -21 -3 -21 -3 -17 -49 c-21 -58\n-43 -83 -86 -101 l-32 -13 -15 -31 c-9 -16 -16 -36 -16 -44 0 -8 -12 -23 -27\n-32 -16 -10 -31 -24 -35 -30 -4 -7 -23 -16 -43 -19 -19 -4 -38 -13 -42 -20 -5\n-7 -33 -25 -63 -39 -71 -35 -120 -71 -135 -100 l-11 -22 -29 0 c-17 0 -44 -10\n-62 -22 -17 -13 -36 -23 -43 -23 l-11 0 6 -20 6 -19 -28 -8 c-15 -4 -35 -6\n-45 -5 -9 2 -22 -6 -30 -17 -12 -17 -55 -40 -130 -70 -30 -11 -38 -31 -14 -31\n8 0 18 -5 22 -11 l7 -12 -39 5 -38 4 -42 -28 c-24 -16 -44 -33 -46 -38 -2 -6\n-36 -21 -75 -35 -101 -34 -125 -48 -171 -100 -42 -48 -71 -57 -79 -26 -6 24\n-32 36 -47 21 l-10 -10 18 -13 18 -13 -41 -37 c-23 -20 -48 -37 -57 -37 -9 0\n-51 -16 -93 -36 l-76 -37 -38 -40 -38 -41 -21 13 -22 13 -25 -30 c-29 -33\n-103 -82 -125 -82 -8 0 -28 -12 -45 -26 -17 -14 -66 -44 -108 -66 l-77 -40\n-29 5 c-15 2 -30 0 -33 -5 -3 -4 2 -8 10 -8 l16 0 0 -20 0 -20 -17 0 c-23 0\n-143 -60 -143 -71 0 -5 -10 -9 -22 -9 -48 -1 -203 -110 -178 -125 6 -4 8 -11\n5 -16 -8 -13 -49 -11 -62 2 l-12 12 -23 -7 c-13 -3 -49 -31 -82 -61 -32 -30\n-67 -55 -77 -55 -10 0 -20 -4 -23 -9 -3 -5 -46 -28 -96 -52 l-90 -42 2 -15 3\n-14 -49 -18 c-26 -11 -56 -26 -65 -34 -10 -9 -26 -16 -37 -16 -27 -1 -59 -21\n-85 -54 -24 -31 -90 -65 -126 -66 l-21 0 -11 -31 -10 -31 -20 7 c-18 5 -32 1\n-81 -26 -77 -43 -100 -49 -200 -52 l-65 -2 -37 38 c-36 37 -43 53 -38 87 2 16\n-17 85 -31 112 -5 10 -9 29 -9 42 l0 24 -16 6 -16 6 6 15 c3 9 2 23 -3 33 -24\n45 -41 107 -41 152 0 27 -4 51 -9 54 -5 3 -11 20 -14 38 -5 34 -22 88 -39 121\n-5 10 -6 22 -3 27 3 5 0 20 -6 32 -17 30 -30 104 -36 200 l-4 77 20 20 c12 12\n21 28 21 36 l0 15 26 6 26 7 9 26 10 26 116 55 c63 30 125 60 137 67 11 6 47\n23 78 38 31 15 67 38 79 51 12 13 30 24 40 24 10 0 21 5 24 10 3 6 14 10 24\n10 9 0 32 16 51 35 18 19 46 37 61 40 15 4 33 13 40 21 6 8 17 14 23 14 7 0\n23 11 36 25 13 14 29 25 36 25 7 0 29 14 47 30 19 17 39 30 45 30 6 0 25 20\n42 45 17 25 37 45 43 45 7 0 18 5 25 12 l12 12 -12 13 -11 14 18 17 17 17 4\n-10 c3 -5 9 -17 14 -25 l10 -15 29 31 c15 17 43 39 62 49 l34 17 115 8 c115 9\n225 31 244 50 5 5 23 10 38 10 l29 0 49 47 c28 26 50 55 51 63 l0 15 9 -22 c5\n-13 16 -23 25 -23 l15 0 0 25 c0 15 -7 39 -15 55 l-15 29 11 7 12 7 9 -17 c15\n-26 29 -8 28 37 0 23 3 63 6 90 l6 47 -16 25 c-20 31 -21 61 -1 69 8 3 15 14\n15 25 0 10 7 21 14 24 8 3 20 24 26 46 l11 41 19 10 c11 6 30 29 41 52 l22 42\n0 63 1 63 -27 28 c-15 15 -27 34 -27 42 0 7 -15 31 -33 54 -19 23 -36 49 -40\n59 l-6 17 -23 0 -23 1 20 15 20 14 -12 19 -11 18 13 14 14 14 -9 40 c-5 22\n-10 48 -10 59 l0 18 15 -6 15 -5 10 19 c14 27 12 72 -4 96 -8 11 -19 39 -25\n62 l-12 42 -25 -6 -25 -6 -17 14 c-9 8 -25 18 -35 21 -21 8 -18 31 5 35 l15 3\n6 -21 5 -20 21 0 c12 0 21 2 21 5 0 15 -34 55 -46 55 -8 0 -38 23 -66 52 l-53\n51 0 21 0 21 -23 10 -22 10 -24 50 c-13 27 -27 57 -31 65 -3 8 -12 26 -20 40\nl-14 24 10 12 10 13 -10 20 c-6 11 -11 28 -11 39 0 11 -13 36 -30 57 -16 21\n-30 46 -30 56 0 11 -9 36 -20 56 -11 21 -20 45 -20 53 0 9 -9 35 -20 58 -11\n22 -20 47 -20 54 0 7 -11 36 -25 63 -13 28 -25 58 -25 68 0 26 -27 74 -45 80\nl-15 6 0 25 c0 14 -7 31 -15 40 -8 8 -15 22 -15 31 0 8 -22 60 -50 114 -27 54\n-50 108 -50 121 0 12 -10 30 -22 40 l-23 18 3 153 3 153 11 13 10 12 -3 90 -4\n90 12 12 13 13 -15 16 -16 17 15 12 c8 7 12 19 9 27 -3 8 0 28 7 44 l12 30 19\n0 c10 0 23 -9 29 -20z m-7231 -5009 c22 -22 48 -55 57 -73 9 -18 23 -37 31\n-41 l14 -8 -6 -19 -6 -18 36 -29 35 -28 0 -23 c0 -12 6 -25 14 -28 8 -3 24\n-25 35 -50 12 -24 26 -44 31 -44 6 0 16 -12 22 -27 7 -16 17 -35 24 -43 19\n-24 38 -93 35 -127 -2 -31 11 -88 30 -125 13 -25 11 -131 -2 -165 -6 -15 -11\n-76 -11 -136 l0 -107 11 -13 c6 -8 11 -27 11 -43 0 -53 12 -129 22 -142 6 -7\n8 -15 5 -19 -4 -3 -2 -13 3 -22 4 -9 11 -57 14 -107 l5 -90 15 -29 c9 -17 16\n-39 16 -51 0 -11 8 -28 18 -38 17 -15 46 -66 87 -156 44 -97 93 -169 153 -223\n12 -11 22 -27 22 -34 0 -7 8 -13 18 -13 l19 0 31 -48 c17 -27 51 -67 76 -90\nl44 -41 21 6 21 5 43 -28 c23 -16 44 -32 45 -37 6 -12 181 -97 201 -97 10 0\n30 -11 45 -25 15 -14 37 -25 50 -25 13 0 31 -9 40 -19 l17 -19 116 -18 116\n-18 26 -22 c14 -12 53 -35 86 -52 l60 -30 43 -46 c23 -25 42 -50 42 -55 0 -5\n14 -14 30 -20 33 -11 39 -25 15 -35 l-16 -6 15 -29 c9 -16 20 -31 26 -33 l11\n-3 -1 -80 c-1 -44 -3 -99 -3 -122 l-1 -43 -13 -4 c-7 -3 -15 -1 -18 4 -11 18\n-22 10 -50 -35 l-28 -44 -66 -32 c-36 -17 -79 -35 -96 -38 l-30 -7 -3 -19 c-6\n-33 -16 -37 -103 -42 -84 -5 -109 -14 -109 -39 0 -16 -14 -18 -40 -4 l-18 10\n-39 -16 -38 -17 -153 6 -154 6 -14 12 -14 11 -30 -12 c-35 -15 -54 -5 -27 14\n9 7 17 18 17 24 0 17 -20 25 -34 13 -7 -6 -37 -13 -67 -17 l-55 -6 -35 17\nc-19 9 -41 26 -50 38 l-16 20 -24 -17 c-32 -22 -36 -21 -77 22 -39 41 -93 72\n-124 72 l-18 0 -9 23 c-5 13 -29 40 -55 60 l-45 38 5 13 5 13 -33 13 c-18 7\n-50 29 -72 49 l-39 36 -4 -12 c-7 -18 -23 -16 -23 2 0 23 -26 69 -74 133 -24\n31 -54 82 -67 112 -12 30 -27 58 -32 61 l-9 7 7 30 6 31 -20 20 c-12 12 -28\n21 -36 21 -17 0 -18 6 -15 77 l2 53 -16 6 -16 6 0 31 0 30 20 -18 20 -18 0 36\n0 37 -15 0 -15 0 0 43 c0 24 -7 58 -16 76 l-16 32 6 187 c3 103 5 196 4 207 0\n11 -1 43 0 70 1 28 0 61 0 75 -1 14 -1 82 0 153 4 256 3 347 -5 582 l-9 240 9\n6 9 6 -1 261 -2 260 10 6 c11 7 52 90 50 102 -6 36 14 31 60 -15z\"/>\n        <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M10265 12395 c-203 -58 -416 -200 -683 -458 -87 -83 -83 -91 66 -145\n221 -82 589 -281 657 -356 6 -6 30 -27 55 -45 25 -19 50 -39 55 -45 6 -6 43\n-39 82 -74 l72 -64 18 4 18 3 3 280 2 280 -21 149 c-22 156 -56 294 -90 359\n-10 21 -19 49 -19 62 l0 25 -16 0 c-9 0 -29 11 -46 25 -35 30 -49 30 -153 0z\"/>\n        <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M5962 12390 c-131 -31 -198 -273 -209 -760 -5 -236 6 -462 26 -498\nl11 -21 20 7 c10 4 56 43 102 87 158 153 272 240 483 366 130 79 321 179 341\n179 19 0 141 57 149 70 l7 11 -115 112 c-330 322 -636 490 -815 447z\"/>\n        <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M9545 10266 c-137 -27 -228 -78 -324 -181 l-64 -68 -34 -75 c-19 -40\n-39 -101 -45 -135 l-10 -62 6 -95 5 -95 25 -62 c75 -192 190 -304 370 -361\nl51 -16 135 -1 135 0 68 28 c88 36 134 65 206 133 l60 56 42 80 c183 352 -6\n754 -402 852 -75 18 -139 19 -224 2z m20 -249 l40 -23 22 -45 22 -44 1 -51 c1\n-223 -301 -283 -375 -75 -35 99 0 196 85 242 l35 18 65 0 65 0 40 -22z\"/>\n        <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M6730 10202 c-30 -10 -82 -31 -115 -47 l-60 -29 -72 -72 -71 -72 -44\n-89 -43 -88 -3 -132 -4 -132 22 -65 c12 -36 26 -68 31 -71 5 -4 9 -14 9 -24 0\n-22 77 -129 123 -172 71 -65 194 -120 316 -140 l66 -10 85 11 85 12 75 31 c86\n35 102 46 187 129 l63 61 39 81 40 81 12 74 13 73 -13 100 c-18 147 -57 229\n-156 333 l-70 73 -65 32 c-36 17 -90 40 -120 51 l-54 19 -111 -1 -110 -1 -55\n-16z m69 -207 c16 -8 43 -32 60 -54 l31 -39 10 -50 9 -50 -9 -33 c-5 -19 -24\n-51 -41 -73 l-32 -38 -44 -19 c-85 -36 -174 -13 -230 59 l-28 37 0 80 0 80 23\n37 22 36 43 21 42 21 58 0 58 0 28 -15z\"/>\n        <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M8215 9393 c-16 -2 -46 -9 -67 -14 l-36 -9 -31 -35 -31 -36 0 -37 0\n-38 29 -45 28 -45 77 -58 76 -57 0 -28 0 -28 -29 -44 -30 -44 -33 -16 c-98\n-48 -130 -50 -199 -12 l-49 26 -10 37 c-6 21 -17 43 -24 49 l-15 12 -42 -3\n-43 -3 -10 -33 -9 -32 11 -38 11 -37 53 -53 c29 -30 68 -60 87 -68 l33 -14 79\n0 78 0 54 24 c30 13 72 40 94 60 50 45 69 45 113 -1 19 -19 59 -46 88 -59 l53\n-24 73 0 73 0 49 26 c96 50 154 123 154 192 l0 40 -25 16 c-41 27 -105 4 -105\n-38 0 -7 -15 -29 -32 -49 l-33 -37 -46 -12 -46 -11 -49 13 -48 14 -43 43 c-46\n45 -63 78 -63 118 l0 24 57 38 c31 21 74 59 96 86 l40 47 5 55 4 55 -35 34\n-35 33 -70 11 c-68 12 -164 13 -227 5z\"/>\n    </group>\n</vector>"
  },
  {
    "path": "composeApp/src/androidMain/res/drawable/ic_splash.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"@drawable/app_icon\"\n    android:inset=\"48dp\"\n    />"
  },
  {
    "path": "composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "composeApp/src/androidMain/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#101010</color>\n</resources>"
  },
  {
    "path": "composeApp/src/androidMain/res/values/splash.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.GitHubStore.Splash\" parent=\"Theme.SplashScreen\">\n        <item name=\"windowSplashScreenAnimatedIcon\">@drawable/ic_splash</item>\n        <item name=\"windowSplashScreenBackground\">@color/ic_launcher_background</item>\n        <item name=\"postSplashScreenTheme\">@style/Theme.AppCompat.DayNight.NoActionBar</item>\n    </style>\n</resources>"
  },
  {
    "path": "composeApp/src/androidMain/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">GitHub Store</string>\n</resources>"
  },
  {
    "path": "composeApp/src/androidMain/res/xml/filepaths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <external-files-path\n        name=\"ghs_downloads\"\n        path=\"/\" />\n\n    <cache-path\n        name=\"exports\"\n        path=\"exports/\" />\n\n</paths>"
  },
  {
    "path": "composeApp/src/androidMain/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"false\">\n        <trust-anchors>\n            <certificates src=\"system\" />\n        </trust-anchors>\n    </base-config>\n\n    <domain-config cleartextTrafficPermitted=\"false\">\n        <domain includeSubdomains=\"true\">github.com</domain>\n        <domain includeSubdomains=\"true\">api.github.com</domain>\n        <trust-anchors>\n            <certificates src=\"system\" />\n        </trust-anchors>\n    </domain-config>\n</network-security-config>"
  },
  {
    "path": "composeApp/src/commonMain/composeResources/drawable/ic_github.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:viewportHeight=\"20\" android:viewportWidth=\"20\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"#000000\" android:fillType=\"evenOdd\" android:pathData=\"M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0\" android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    \n</vector>\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt",
    "content": "package zed.rainxch.githubstore\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport androidx.navigation.compose.rememberNavController\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ApplyAndroidSystemBars\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.githubstore.app.components.RateLimitDialog\nimport zed.rainxch.githubstore.app.components.SessionExpiredDialog\nimport zed.rainxch.githubstore.app.deeplink.DeepLinkDestination\nimport zed.rainxch.githubstore.app.deeplink.DeepLinkParser\nimport zed.rainxch.githubstore.app.desktop.KeyboardNavigation\nimport zed.rainxch.githubstore.app.desktop.KeyboardNavigationEvent\nimport zed.rainxch.githubstore.app.navigation.AppNavigation\nimport zed.rainxch.githubstore.app.navigation.GithubStoreGraph\nimport zed.rainxch.githubstore.app.navigation.getCurrentScreen\n\n@Composable\nfun App(deepLinkUri: String? = null) {\n    val viewModel: MainViewModel = koinViewModel()\n    val state by viewModel.state.collectAsStateWithLifecycle()\n\n    val navController = rememberNavController()\n    val currentScreen = navController.currentBackStackEntryAsState().value.getCurrentScreen()\n\n    LaunchedEffect(deepLinkUri) {\n        deepLinkUri?.let { uri ->\n            when (val destination = DeepLinkParser.parse(uri)) {\n                is DeepLinkDestination.Repository -> {\n                    navController.navigate(\n                        GithubStoreGraph.DetailsScreen(\n                            owner = destination.owner,\n                            repo = destination.repo,\n                        ),\n                    )\n                }\n\n                DeepLinkDestination.None -> {\n                    // ignore unrecognized deep links\n                }\n            }\n        }\n    }\n\n    ObserveAsEvents(KeyboardNavigation.events) { event ->\n        when (event) {\n            KeyboardNavigationEvent.OnCtrlFClick -> {\n                if (currentScreen !is GithubStoreGraph.SearchScreen) {\n                    navController.navigate(GithubStoreGraph.SearchScreen) {\n                        popUpTo(GithubStoreGraph.HomeScreen) {\n                            saveState = true\n                        }\n\n                        launchSingleTop = true\n                        restoreState = true\n                    }\n                }\n            }\n        }\n    }\n\n    GithubStoreTheme(\n        fontTheme = state.currentFontTheme,\n        appTheme = state.currentColorTheme,\n        isAmoledTheme = state.isAmoledTheme,\n        isDarkTheme = state.isDarkTheme ?: isSystemInDarkTheme(),\n    ) {\n        ApplyAndroidSystemBars(state.isDarkTheme)\n\n        if (state.showRateLimitDialog && state.rateLimitInfo != null) {\n            RateLimitDialog(\n                rateLimitInfo = state.rateLimitInfo!!,\n                isAuthenticated = state.isLoggedIn,\n                onDismiss = {\n                    viewModel.onAction(MainAction.DismissRateLimitDialog)\n                },\n                onSignIn = {\n                    viewModel.onAction(MainAction.DismissRateLimitDialog)\n\n                    navController.navigate(GithubStoreGraph.AuthenticationScreen)\n                },\n            )\n        }\n\n        if (state.showSessionExpiredDialog) {\n            SessionExpiredDialog(\n                onDismiss = {\n                    viewModel.onAction(MainAction.DismissSessionExpiredDialog)\n                },\n                onSignIn = {\n                    viewModel.onAction(MainAction.DismissSessionExpiredDialog)\n                    navController.navigate(GithubStoreGraph.AuthenticationScreen)\n                },\n            )\n        }\n\n        AppNavigation(\n            navController = navController,\n            isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n        )\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainAction.kt",
    "content": "package zed.rainxch.githubstore\n\nsealed interface MainAction {\n    data object DismissRateLimitDialog : MainAction\n\n    data object DismissSessionExpiredDialog : MainAction\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt",
    "content": "package zed.rainxch.githubstore\n\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.domain.model.RateLimitInfo\n\ndata class MainState(\n    val isLoggedIn: Boolean = false,\n    val rateLimitInfo: RateLimitInfo? = null,\n    val showRateLimitDialog: Boolean = false,\n    val showSessionExpiredDialog: Boolean = false,\n    val currentColorTheme: AppTheme = AppTheme.OCEAN,\n    val isAmoledTheme: Boolean = false,\n    val isDarkTheme: Boolean? = null,\n    val currentFontTheme: FontTheme = FontTheme.CUSTOM,\n    val isLiquidGlassEnabled: Boolean = true,\n)\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt",
    "content": "package zed.rainxch.githubstore\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport zed.rainxch.core.domain.repository.AuthenticationState\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.RateLimitRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\n\nclass MainViewModel(\n    private val tweaksRepository: TweaksRepository,\n    private val installedAppsRepository: InstalledAppsRepository,\n    private val authenticationState: AuthenticationState,\n    private val rateLimitRepository: RateLimitRepository,\n    private val syncUseCase: SyncInstalledAppsUseCase,\n) : ViewModel() {\n    private val _state = MutableStateFlow(MainState())\n    val state = _state.asStateFlow()\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            authenticationState\n                .isUserLoggedIn()\n                .collect { isLoggedIn ->\n                    _state.update { it.copy(isLoggedIn = isLoggedIn) }\n\n                    if (isLoggedIn) {\n                        rateLimitRepository.clear()\n                    }\n                }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository\n                .getThemeColor()\n                .collect { theme ->\n                    _state.update {\n                        it.copy(currentColorTheme = theme)\n                    }\n                }\n        }\n        viewModelScope.launch {\n            tweaksRepository\n                .getAmoledTheme()\n                .collect { isAmoled ->\n                    _state.update {\n                        it.copy(isAmoledTheme = isAmoled)\n                    }\n                }\n        }\n        viewModelScope.launch {\n            tweaksRepository\n                .getIsDarkTheme()\n                .collect { isDarkTheme ->\n                    _state.update {\n                        it.copy(isDarkTheme = isDarkTheme)\n                    }\n                }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository\n                .getFontTheme()\n                .collect { fontTheme ->\n                    _state.update {\n                        it.copy(currentFontTheme = fontTheme)\n                    }\n                }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository.getLiquidGlassEnabled().collect { enabled ->\n                _state.update { it.copy(isLiquidGlassEnabled = enabled) }\n            }\n        }\n\n        viewModelScope.launch {\n            rateLimitRepository.rateLimitState.collect { rateLimitInfo ->\n                _state.update { currentState ->\n                    currentState.copy(rateLimitInfo = rateLimitInfo)\n                }\n            }\n        }\n\n        viewModelScope.launch {\n            rateLimitRepository.rateLimitExhaustedEvent.collect { info ->\n                _state.update { it.copy(showRateLimitDialog = true, rateLimitInfo = info) }\n            }\n        }\n\n        viewModelScope.launch {\n            authenticationState.sessionExpiredEvent.collect {\n                _state.update { it.copy(showSessionExpiredDialog = true) }\n            }\n        }\n\n        viewModelScope.launch(Dispatchers.IO) {\n            syncUseCase().onSuccess {\n                installedAppsRepository.checkAllForUpdates()\n            }\n        }\n    }\n\n    fun onAction(action: MainAction) {\n        when (action) {\n            MainAction.DismissRateLimitDialog -> {\n                _state.update { it.copy(showRateLimitDialog = false) }\n            }\n\n            MainAction.DismissSessionExpiredDialog -> {\n                _state.update { it.copy(showSessionExpiredDialog = false) }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/RateLimitDialog.kt",
    "content": "package zed.rainxch.githubstore.app.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Warning\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.RateLimitInfo\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_close\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_ok\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_resets_in_minutes\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_sign_in\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_tip_sign_in\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_used_all\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_used_all_free\n\n@Composable\nfun RateLimitDialog(\n    rateLimitInfo: RateLimitInfo,\n    isAuthenticated: Boolean,\n    onDismiss: () -> Unit,\n    onSignIn: () -> Unit,\n) {\n    val timeUntilReset =\n        remember(rateLimitInfo) {\n            rateLimitInfo.timeUntilReset().inWholeMinutes.toInt()\n        }\n\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        icon = {\n            Icon(\n                imageVector = Icons.Default.Warning,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.error,\n            )\n        },\n        title = {\n            Text(\n                text = stringResource(Res.string.rate_limit_exceeded),\n                style = MaterialTheme.typography.headlineSmall,\n                fontWeight = FontWeight.Black,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n        },\n        text = {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                Text(\n                    text =\n                        if (isAuthenticated) {\n                            stringResource(\n                                Res.string.rate_limit_used_all,\n                                rateLimitInfo.limit,\n                            )\n                        } else {\n                            stringResource(\n                                Res.string.rate_limit_used_all_free,\n                                60,\n                            )\n                        },\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.outline,\n                )\n\n                Text(\n                    text =\n                        stringResource(\n                            Res.string.rate_limit_resets_in_minutes,\n                            timeUntilReset,\n                        ),\n                    style = MaterialTheme.typography.bodyMedium,\n                    fontWeight = FontWeight.Bold,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n\n                if (!isAuthenticated) {\n                    Spacer(modifier = Modifier.height(8.dp))\n\n                    Text(\n                        text = stringResource(Res.string.rate_limit_tip_sign_in),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            }\n        },\n        confirmButton = {\n            if (!isAuthenticated) {\n                Button(onClick = onSignIn) {\n                    Text(\n                        text = stringResource(Res.string.rate_limit_sign_in),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onPrimary,\n                    )\n                }\n            } else {\n                Button(onClick = onDismiss) {\n                    Text(\n                        text = stringResource(Res.string.rate_limit_ok),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                }\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(\n                    text = stringResource(Res.string.rate_limit_close),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n            }\n        },\n    )\n}\n\n@Preview\n@Composable\nfun RateLimitDialogPreview() {\n    GithubStoreTheme {\n        RateLimitDialog(\n            rateLimitInfo =\n                RateLimitInfo(\n                    limit = 1000,\n                    remaining = 2000,\n                    resetTimestamp = 0L,\n                ),\n            isAuthenticated = false,\n            onDismiss = {\n            },\n            onSignIn = {\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/SessionExpiredDialog.kt",
    "content": "package zed.rainxch.githubstore.app.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.LockOpen\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun SessionExpiredDialog(\n    onDismiss: () -> Unit,\n    onSignIn: () -> Unit,\n) {\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        icon = {\n            Icon(\n                imageVector = Icons.Default.LockOpen,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.error,\n            )\n        },\n        title = {\n            Text(\n                text = stringResource(Res.string.session_expired_title),\n                style = MaterialTheme.typography.headlineSmall,\n                fontWeight = FontWeight.Black,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n        },\n        text = {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                Text(\n                    text = stringResource(Res.string.session_expired_message),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.outline,\n                )\n\n                Text(\n                    text = stringResource(Res.string.session_expired_hint),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n        },\n        confirmButton = {\n            Button(onClick = onSignIn) {\n                Text(\n                    text = stringResource(Res.string.sign_in_again),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onPrimary,\n                )\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(\n                    text = stringResource(Res.string.continue_as_guest),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt",
    "content": "package zed.rainxch.githubstore.app.deeplink\n\nsealed interface DeepLinkDestination {\n    data class Repository(\n        val owner: String,\n        val repo: String,\n    ) : DeepLinkDestination\n\n    data object None : DeepLinkDestination\n}\n\nobject DeepLinkParser {\n    private val INVALID_CHARS = setOf('/', '\\\\', '?', '#', '@', ':', '*', '\"', '<', '>', '|', '%', '&', '=')\n\n    private val FORBIDDEN_PATTERNS = listOf(\"..\", \"~\", \"\\u0000\")\n\n    private val EXCLUDED_PATHS =\n        setOf(\n            \"about\",\n            \"account\",\n            \"admin\",\n            \"api\",\n            \"apps\",\n            \"articles\",\n            \"blog\",\n            \"business\",\n            \"collections\",\n            \"contact\",\n            \"dashboard\",\n            \"enterprises\",\n            \"events\",\n            \"explore\",\n            \"features\",\n            \"home\",\n            \"issues\",\n            \"marketplace\",\n            \"new\",\n            \"notifications\",\n            \"orgs\",\n            \"pricing\",\n            \"pulls\",\n            \"search\",\n            \"security\",\n            \"settings\",\n            \"showcases\",\n            \"site\",\n            \"sponsors\",\n            \"topics\",\n            \"trending\",\n            \"team\",\n        )\n\n    fun parse(uri: String): DeepLinkDestination {\n        return when {\n            uri.startsWith(\"githubstore://repo/\") -> {\n                val path = uri.removePrefix(\"githubstore://repo/\")\n                val decoded = urlDecode(path)\n                parseOwnerRepo(decoded)\n            }\n\n            uri.startsWith(\"https://github.com/\") -> {\n                val path =\n                    uri\n                        .removePrefix(\"https://github.com/\")\n                        .substringBefore('?')\n                        .substringBefore('#')\n                val decoded = urlDecode(path)\n\n                val parts = decoded.split(\"/\").filter { it.isNotEmpty() }\n                if (parts.size >= 2) {\n                    val owner = parts[0]\n                    val repo = parts[1]\n                    if (isStrictlyValidOwnerRepo(owner, repo)) {\n                        return DeepLinkDestination.Repository(owner, repo)\n                    }\n                }\n                DeepLinkDestination.None\n            }\n\n            uri.startsWith(\"https://github-store.org/app/\") -> {\n                extractQueryParam(uri, \"repo\")?.let { encodedRepoParam ->\n                    val decoded = urlDecode(encodedRepoParam)\n                    parseOwnerRepo(decoded)\n                } ?: DeepLinkDestination.None\n            }\n\n            else -> {\n                DeepLinkDestination.None\n            }\n        }\n    }\n\n    /**\n     * URL-decode a string, handling percent-encoded characters.\n     * Returns the original string if decoding fails.\n     */\n    private fun urlDecode(value: String): String =\n        try {\n            val result = StringBuilder()\n            var i = 0\n            while (i < value.length) {\n                when (val c = value[i]) {\n                    '%' -> {\n                        if (i + 2 < value.length) {\n                            val hex = value.substring(i + 1, i + 3)\n                            val code = hex.toIntOrNull(16)\n                            if (code != null) {\n                                result.append(code.toChar())\n                                i += 3\n                                continue\n                            }\n                        }\n                        result.append(c)\n                        i++\n                    }\n\n                    '+' -> {\n                        result.append(' ')\n                        i++\n                    }\n\n                    else -> {\n                        result.append(c)\n                        i++\n                    }\n                }\n            }\n            result.toString()\n        } catch (_: Exception) {\n            value\n        }\n\n    private fun parseOwnerRepo(path: String): DeepLinkDestination {\n        val parts = path.split(\"/\").filter { it.isNotEmpty() }\n        return if (parts.size >= 2) {\n            val owner = parts[0]\n            val repo = parts[1]\n            if (isStrictlyValidOwnerRepo(owner, repo)) {\n                DeepLinkDestination.Repository(owner, repo)\n            } else {\n                DeepLinkDestination.None\n            }\n        } else {\n            DeepLinkDestination.None\n        }\n    }\n\n    /**\n     * Strictly validate owner and repo names to prevent injection attacks.\n     * Rejects:\n     * - Empty strings\n     * - Special characters that could be used for injection\n     * - Path traversal patterns\n     * - Control characters and whitespace\n     * - Excluded GitHub paths (like 'about', 'settings', etc.)\n     * - Names that exceed GitHub's length limits\n     * - Names that don't start with alphanumeric characters\n     */\n    private fun isStrictlyValidOwnerRepo(\n        owner: String,\n        repo: String,\n    ): Boolean {\n        if (owner.isEmpty() || repo.isEmpty()) {\n            return false\n        }\n\n        if (owner.any { it in INVALID_CHARS } || repo.any { it in INVALID_CHARS }) {\n            return false\n        }\n\n        if (FORBIDDEN_PATTERNS.any { pattern ->\n                owner.contains(pattern, ignoreCase = true) ||\n                    repo.contains(pattern, ignoreCase = true)\n            }\n        ) {\n            return false\n        }\n\n        if (owner.any { it.isISOControl() } || repo.any { it.isISOControl() }) {\n            return false\n        }\n\n        if (owner.contains(' ') || repo.contains(' ')) {\n            return false\n        }\n\n        if (EXCLUDED_PATHS.contains(owner.lowercase())) {\n            return false\n        }\n\n        if (owner.length > 39 || repo.length > 100) {\n            return false\n        }\n\n        if (!owner.first().isLetterOrDigit() || !repo.first().isLetterOrDigit()) {\n            return false\n        }\n\n        return true\n    }\n\n    private fun extractQueryParam(\n        uri: String,\n        key: String,\n    ): String? {\n        val queryStart = uri.indexOf('?')\n        if (queryStart == -1) return null\n\n        val queryString = uri.substring(queryStart + 1)\n        val params = queryString.split('&')\n\n        for (param in params) {\n            val keyValue = param.split('=', limit = 2)\n            if (keyValue.size == 2 && keyValue[0] == key) {\n                return keyValue[1]\n            }\n        }\n        return null\n    }\n\n    fun extractSupportedUrl(text: String): String? {\n        val regex =\n            \"\"\"https?://(?:www\\.)?(?:github\\.com|github-store\\.org)(?=[/\\s?#]|$)[^\\s<>\"')\\],;.!]*\"\"\".toRegex(\n                RegexOption.IGNORE_CASE,\n            )\n        return regex.find(text)?.value\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/desktop/KeyboardNavigation.kt",
    "content": "package zed.rainxch.githubstore.app.desktop\n\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.receiveAsFlow\n\nobject KeyboardNavigation {\n    private val _events = Channel<KeyboardNavigationEvent>()\n    val events = _events.receiveAsFlow()\n\n    fun onKeyClicked(event: KeyboardNavigationEvent) {\n        _events.trySend(event)\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/desktop/KeyboardNavigationEvent.kt",
    "content": "package zed.rainxch.githubstore.app.desktop\n\nsealed interface KeyboardNavigationEvent {\n    data object OnCtrlFClick : KeyboardNavigationEvent\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt",
    "content": "package zed.rainxch.githubstore.app.di\n\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.viewModel\nimport org.koin.dsl.module\nimport zed.rainxch.githubstore.MainViewModel\n\nval mainModule: Module =\n    module {\n        viewModel {\n            MainViewModel(\n                tweaksRepository = get(),\n                installedAppsRepository = get(),\n                rateLimitRepository = get(),\n                syncUseCase = get(),\n                authenticationState = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt",
    "content": "package zed.rainxch.githubstore.app.di\n\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.module\nimport zed.rainxch.apps.presentation.AppsViewModel\nimport zed.rainxch.auth.presentation.AuthenticationViewModel\nimport zed.rainxch.details.presentation.DetailsViewModel\nimport zed.rainxch.devprofile.presentation.DeveloperProfileViewModel\nimport zed.rainxch.favourites.presentation.FavouritesViewModel\nimport zed.rainxch.home.presentation.HomeViewModel\nimport zed.rainxch.profile.presentation.ProfileViewModel\nimport zed.rainxch.search.presentation.SearchViewModel\nimport zed.rainxch.starred.presentation.StarredReposViewModel\n\nval viewModelsModule =\n    module {\n        viewModelOf(::AppsViewModel)\n        viewModelOf(::AuthenticationViewModel)\n        viewModelOf(::DetailsViewModel)\n        viewModelOf(::DeveloperProfileViewModel)\n        viewModelOf(::FavouritesViewModel)\n        viewModelOf(::HomeViewModel)\n        viewModelOf(::SearchViewModel)\n        viewModelOf(::ProfileViewModel)\n        viewModelOf(::StarredReposViewModel)\n    }\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt",
    "content": "package zed.rainxch.githubstore.app.di\n\nimport org.koin.core.context.startKoin\nimport org.koin.dsl.KoinAppDeclaration\nimport zed.rainxch.apps.data.di.appsModule\nimport zed.rainxch.auth.data.di.authModule\nimport zed.rainxch.core.data.di.coreModule\nimport zed.rainxch.core.data.di.corePlatformModule\nimport zed.rainxch.core.data.di.databaseModule\nimport zed.rainxch.core.data.di.networkModule\nimport zed.rainxch.details.data.di.detailsModule\nimport zed.rainxch.devprofile.data.di.devProfileModule\nimport zed.rainxch.home.data.di.homeModule\nimport zed.rainxch.profile.data.di.settingsModule\nimport zed.rainxch.search.data.di.searchModule\n\nfun initKoin(config: KoinAppDeclaration? = null) {\n    startKoin {\n        config?.invoke(this)\n        modules(\n            mainModule,\n            corePlatformModule,\n            coreModule,\n            networkModule,\n            databaseModule,\n            viewModelsModule,\n            appsModule,\n            authModule,\n            detailsModule,\n            devProfileModule,\n            homeModule,\n            searchModule,\n            settingsModule,\n        )\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt",
    "content": "package zed.rainxch.githubstore.app.navigation\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport androidx.navigation.toRoute\nimport io.github.fletchmckee.liquid.rememberLiquidState\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\nimport zed.rainxch.apps.presentation.AppsRoot\nimport zed.rainxch.apps.presentation.AppsViewModel\nimport zed.rainxch.auth.presentation.AuthenticationRoot\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid\nimport zed.rainxch.details.presentation.DetailsRoot\nimport zed.rainxch.devprofile.presentation.DeveloperProfileRoot\nimport zed.rainxch.favourites.presentation.FavouritesRoot\nimport zed.rainxch.home.presentation.HomeRoot\nimport zed.rainxch.profile.presentation.ProfileRoot\nimport zed.rainxch.search.presentation.SearchRoot\nimport zed.rainxch.starred.presentation.StarredReposRoot\n\n@Composable\nfun AppNavigation(\n    navController: NavHostController,\n    isLiquidGlassEnabled: Boolean = true,\n) {\n    val liquidState = rememberLiquidState()\n    var bottomNavigationHeight by remember { mutableStateOf(0.dp) }\n    val density = LocalDensity.current\n\n    val appsViewModel = koinViewModel<AppsViewModel>()\n    val appsState by appsViewModel.state.collectAsStateWithLifecycle()\n\n    CompositionLocalProvider(\n        LocalBottomNavigationLiquid provides liquidState,\n        LocalBottomNavigationHeight provides bottomNavigationHeight,\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n        ) {\n            NavHost(\n                navController = navController,\n                startDestination = GithubStoreGraph.HomeScreen,\n                modifier = Modifier.background(MaterialTheme.colorScheme.background),\n            ) {\n                composable<GithubStoreGraph.HomeScreen> {\n                    HomeRoot(\n                        onNavigateToSearch = {\n                            navController.navigate(GithubStoreGraph.SearchScreen)\n                        },\n                        onNavigateToSettings = {\n                            navController.navigate(GithubStoreGraph.ProfileScreen)\n                        },\n                        onNavigateToApps = {\n                            navController.navigate(GithubStoreGraph.AppsScreen)\n                        },\n                        onNavigateToDetails = { repoId ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    repositoryId = repoId,\n                                ),\n                            )\n                        },\n                        onNavigateToDeveloperProfile = { username ->\n                            navController.navigate(\n                                GithubStoreGraph.DeveloperProfileScreen(\n                                    username = username,\n                                ),\n                            )\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.SearchScreen> {\n                    SearchRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onNavigateToDetails = { repoId ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    repositoryId = repoId,\n                                ),\n                            )\n                        },\n                        onNavigateToDetailsFromLink = { owner, repo ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    owner = owner,\n                                    repo = repo,\n                                ),\n                            )\n                        },\n                        onNavigateToDeveloperProfile = { username ->\n                            navController.navigate(\n                                GithubStoreGraph.DeveloperProfileScreen(\n                                    username = username,\n                                ),\n                            )\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.DetailsScreen> { backStackEntry ->\n                    val args = backStackEntry.toRoute<GithubStoreGraph.DetailsScreen>()\n                    DetailsRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onOpenRepositoryInApp = { repoId ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    repositoryId = repoId,\n                                ),\n                            )\n                        },\n                        onNavigateToDeveloperProfile = { username ->\n                            navController.navigate(\n                                GithubStoreGraph.DeveloperProfileScreen(\n                                    username = username,\n                                ),\n                            )\n                        },\n                        viewModel =\n                            koinViewModel {\n                                parametersOf(\n                                    args.repositoryId,\n                                    args.owner,\n                                    args.repo,\n                                    args.isComingFromUpdate,\n                                )\n                            },\n                    )\n                }\n\n                composable<GithubStoreGraph.DeveloperProfileScreen> { backStackEntry ->\n                    val args = backStackEntry.toRoute<GithubStoreGraph.DeveloperProfileScreen>()\n                    DeveloperProfileRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onNavigateToDetails = { repoId ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    repositoryId = repoId,\n                                ),\n                            )\n                        },\n                        viewModel =\n                            koinViewModel {\n                                parametersOf(args.username)\n                            },\n                    )\n                }\n\n                composable<GithubStoreGraph.AuthenticationScreen> {\n                    AuthenticationRoot(\n                        onNavigateToHome = {\n                            navController.navigate(GithubStoreGraph.HomeScreen) {\n                                popUpTo(0) {\n                                    inclusive = true\n                                }\n                            }\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.FavouritesScreen> {\n                    FavouritesRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onNavigateToDetails = {\n                            navController.navigate(GithubStoreGraph.DetailsScreen(it))\n                        },\n                        onNavigateToDeveloperProfile = { username ->\n                            navController.navigate(\n                                GithubStoreGraph.DeveloperProfileScreen(\n                                    username = username,\n                                ),\n                            )\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.StarredReposScreen> {\n                    StarredReposRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onNavigateToDetails = { repoId ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    repositoryId = repoId,\n                                ),\n                            )\n                        },\n                        onNavigateToAuthentication = {\n                            navController.navigate(\n                                GithubStoreGraph.AuthenticationScreen,\n                            )\n                        },\n                        onNavigateToDeveloperProfile = { username ->\n                            navController.navigate(\n                                GithubStoreGraph.DeveloperProfileScreen(\n                                    username = username,\n                                ),\n                            )\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.ProfileScreen> {\n                    ProfileRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onNavigateToAuthentication = {\n                            navController.navigate(GithubStoreGraph.AuthenticationScreen)\n                        },\n                        onNavigateToStarredRepos = {\n                            navController.navigate(GithubStoreGraph.StarredReposScreen)\n                        },\n                        onNavigateToFavouriteRepos = {\n                            navController.navigate(GithubStoreGraph.FavouritesScreen)\n                        },\n                        onNavigateToDevProfile = { username ->\n                            navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username))\n                        },\n                        onNavigateToSponsor = {\n                            navController.navigate(GithubStoreGraph.SponsorScreen)\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.SponsorScreen> {\n                    zed.rainxch.profile.presentation.SponsorScreen(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                    )\n                }\n\n                composable<GithubStoreGraph.AppsScreen> {\n                    AppsRoot(\n                        onNavigateBack = {\n                            navController.navigateUp()\n                        },\n                        onNavigateToRepo = { repoId ->\n                            navController.navigate(\n                                GithubStoreGraph.DetailsScreen(\n                                    repositoryId = repoId,\n                                    isComingFromUpdate = true,\n                                ),\n                            )\n                        },\n                        viewModel = appsViewModel,\n                        state = appsState,\n                    )\n                }\n            }\n\n            val currentScreen =\n                navController.currentBackStackEntryAsState().value.getCurrentScreen()\n\n            currentScreen?.let {\n                BottomNavigation(\n                    currentScreen = currentScreen,\n                    onNavigate = {\n                        navController.navigate(it) {\n                            popUpTo(GithubStoreGraph.HomeScreen) {\n                                saveState = true\n                            }\n\n                            launchSingleTop = true\n                            restoreState = true\n                        }\n                    },\n                    isUpdateAvailable = appsState.apps.any { it.installedApp.isUpdateAvailable },\n                    isLiquidGlassEnabled = isLiquidGlassEnabled,\n                    modifier =\n                        Modifier\n                            .align(Alignment.BottomCenter)\n                            .navigationBarsPadding()\n                            .padding(bottom = 24.dp)\n                            .onGloballyPositioned { coordinates ->\n                                bottomNavigationHeight =\n                                    with(density) { coordinates.size.height.toDp() }\n                            },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt",
    "content": "package zed.rainxch.githubstore.app.navigation\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsPressedAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.luminance\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInParent\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport io.github.fletchmckee.liquid.liquid\nimport io.github.fletchmckee.liquid.rememberLiquidState\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.getPlatform\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.isLiquidFrostAvailable\n\n@Composable\nfun BottomNavigation(\n    currentScreen: GithubStoreGraph,\n    onNavigate: (GithubStoreGraph) -> Unit,\n    isUpdateAvailable: Boolean,\n    isLiquidGlassEnabled: Boolean = true,\n    modifier: Modifier = Modifier,\n) {\n    val liquidState = LocalBottomNavigationLiquid.current\n\n    if (currentScreen !in BottomNavigationUtils.allowedScreens()) return\n\n    val visibleItems =\n        remember {\n            BottomNavigationUtils.items().filterNot {\n                getPlatform() != Platform.ANDROID &&\n                    it.screen == GithubStoreGraph.AppsScreen\n            }\n        }\n\n    val selectedIndex = visibleItems.indexOfFirst { it.screen == currentScreen }\n\n    val itemPositions = remember { mutableMapOf<Int, Pair<Float, Float>>() }\n\n    var selectedItemPos by remember { mutableStateOf<Pair<Float, Float>?>(null) }\n\n    val rowHorizontalPaddingDp = 6.dp\n    val density = LocalDensity.current\n    val rowHorizontalPaddingPx = with(density) { rowHorizontalPaddingDp.toPx() }\n\n    val indicatorHorizontalInsetPx = with(density) { 4.dp.toPx() }\n\n    val indicatorX = remember { Animatable(0f) }\n    val indicatorWidth = remember { Animatable(0f) }\n\n    LaunchedEffect(selectedIndex, selectedItemPos) {\n        val raw = selectedItemPos ?: itemPositions[selectedIndex] ?: return@LaunchedEffect\n        val targetX = raw.first + rowHorizontalPaddingPx - indicatorHorizontalInsetPx\n        val targetW = raw.second + indicatorHorizontalInsetPx * 2f\n        launch {\n            indicatorX.animateTo(\n                targetValue = targetX,\n                animationSpec =\n                    spring(\n                        dampingRatio = Spring.DampingRatioLowBouncy,\n                        stiffness = Spring.StiffnessLow,\n                    ),\n            )\n        }\n        launch {\n            indicatorWidth.animateTo(\n                targetValue = targetW,\n                animationSpec =\n                    spring(\n                        dampingRatio = Spring.DampingRatioNoBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n            )\n        }\n    }\n\n    val isDarkTheme =\n        !MaterialTheme.colorScheme.background\n            .luminance()\n            .let { it > 0.5f }\n\n    Box(\n        modifier = modifier,\n        contentAlignment = Alignment.Center,\n    ) {\n        val useLiquid = isLiquidGlassEnabled && isLiquidFrostAvailable()\n\n        Box(\n            modifier =\n                Modifier\n                    .clip(CircleShape)\n                    .then(\n                        if (useLiquid) {\n                            Modifier\n                                .background(\n                                    MaterialTheme.colorScheme.surfaceContainerHighest.copy(\n                                        alpha = if (isDarkTheme) .25f else .15f,\n                                    ),\n                                ).liquid(liquidState) {\n                                    this.shape = CircleShape\n                                    this.frost = if (isDarkTheme) 12.dp else 10.dp\n                                    this.curve = if (isDarkTheme) .35f else .45f\n                                    this.refraction = if (isDarkTheme) .08f else .12f\n                                    this.dispersion = if (isDarkTheme) .18f else .25f\n                                    this.saturation = if (isDarkTheme) .40f else .55f\n                                    this.contrast = if (isDarkTheme) 1.8f else 1.6f\n                                }\n                        } else {\n                            Modifier\n                                .background(MaterialTheme.colorScheme.surfaceContainer)\n                                .border(\n                                    width = 1.dp,\n                                    color = MaterialTheme.colorScheme.outlineVariant,\n                                    shape = CircleShape,\n                                )\n                        },\n                    ).pointerInput(Unit) { },\n        ) {\n            val glassHighColor =\n                if (isDarkTheme) {\n                    Color.White.copy(alpha = .12f)\n                } else {\n                    Color.White.copy(alpha = .30f)\n                }\n            val glassLowColor =\n                if (isDarkTheme) {\n                    Color.White.copy(alpha = .04f)\n                } else {\n                    Color.White.copy(alpha = .10f)\n                }\n            val specularColor =\n                if (isDarkTheme) {\n                    Color.White.copy(alpha = .18f)\n                } else {\n                    Color.White.copy(alpha = .45f)\n                }\n            val innerGlowColor =\n                if (isDarkTheme) {\n                    Color.White.copy(alpha = .03f)\n                } else {\n                    Color.White.copy(alpha = .08f)\n                }\n            val borderColor =\n                if (isDarkTheme) {\n                    Color.White.copy(alpha = .08f)\n                } else {\n                    Color.Transparent\n                }\n\n            Box(\n                modifier =\n                    Modifier\n                        .matchParentSize()\n                        .drawBehind {\n                            if (indicatorWidth.value > 0f) {\n                                if (isDarkTheme) {\n                                    drawRoundRect(\n                                        color = borderColor,\n                                        topLeft =\n                                            Offset(\n                                                indicatorX.value - .5.dp.toPx(),\n                                                1.5.dp.toPx(),\n                                            ),\n                                        size =\n                                            Size(\n                                                indicatorWidth.value + 1.dp.toPx(),\n                                                size.height - 3.dp.toPx(),\n                                            ),\n                                        cornerRadius = CornerRadius(size.height / 2f),\n                                        style = Stroke(width = 1.dp.toPx()),\n                                    )\n                                }\n\n                                drawRoundRect(\n                                    brush =\n                                        Brush.verticalGradient(\n                                            colors = listOf(glassHighColor, glassLowColor),\n                                        ),\n                                    topLeft = Offset(indicatorX.value, 2.dp.toPx()),\n                                    size = Size(indicatorWidth.value, size.height - 4.dp.toPx()),\n                                    cornerRadius = CornerRadius(size.height / 2f),\n                                )\n\n                                drawRoundRect(\n                                    brush =\n                                        Brush.horizontalGradient(\n                                            colors =\n                                                listOf(\n                                                    Color.Transparent,\n                                                    specularColor,\n                                                    Color.Transparent,\n                                                ),\n                                            startX = indicatorX.value + indicatorWidth.value * .15f,\n                                            endX = indicatorX.value + indicatorWidth.value * .85f,\n                                        ),\n                                    topLeft =\n                                        Offset(\n                                            indicatorX.value + indicatorWidth.value * .15f,\n                                            3.dp.toPx(),\n                                        ),\n                                    size = Size(indicatorWidth.value * .7f, 1.5.dp.toPx()),\n                                    cornerRadius = CornerRadius(1.dp.toPx()),\n                                )\n\n                                drawRoundRect(\n                                    brush =\n                                        Brush.verticalGradient(\n                                            colors = listOf(Color.Transparent, innerGlowColor),\n                                        ),\n                                    topLeft =\n                                        Offset(\n                                            indicatorX.value + 4.dp.toPx(),\n                                            size.height - 8.dp.toPx(),\n                                        ),\n                                    size = Size(indicatorWidth.value - 8.dp.toPx(), 4.dp.toPx()),\n                                    cornerRadius = CornerRadius(2.dp.toPx()),\n                                )\n                            }\n                        },\n            )\n\n            Row(\n                modifier = Modifier.padding(horizontal = rowHorizontalPaddingDp, vertical = 4.dp),\n                horizontalArrangement = Arrangement.spacedBy(4.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                visibleItems.forEachIndexed { index, item ->\n                    LiquidGlassTabItem(\n                        item = item,\n                        hasBadge = item.screen == GithubStoreGraph.AppsScreen && isUpdateAvailable,\n                        isSelected = item.screen == currentScreen,\n                        onSelect = { onNavigate(item.screen) },\n                        onPositioned = { x, width ->\n                            itemPositions[index] = x to width\n                            if (index == selectedIndex) {\n                                selectedItemPos = x to width\n                            }\n                            if (index == selectedIndex && indicatorWidth.value == 0f) {\n                                val snapX = x + rowHorizontalPaddingPx - indicatorHorizontalInsetPx\n                                val snapW = width + indicatorHorizontalInsetPx * 2f\n                                indicatorX.snapTo(snapX)\n                                indicatorWidth.snapTo(snapW)\n                            }\n                        },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LiquidGlassTabItem(\n    item: BottomNavigationItem,\n    isSelected: Boolean,\n    onSelect: () -> Unit,\n    hasBadge: Boolean = false,\n    onPositioned: suspend (x: Float, width: Float) -> Unit,\n) {\n    val scope = rememberCoroutineScope()\n    val density = LocalDensity.current\n    val interactionSource = remember { MutableInteractionSource() }\n    val isPressed by interactionSource.collectIsPressedAsState()\n\n    val pressScale by animateFloatAsState(\n        targetValue = if (isPressed) 0.85f else 1f,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessMedium,\n            ),\n        label = \"pressScale\",\n    )\n\n    val iconScale by animateFloatAsState(\n        targetValue = if (isSelected) 1.15f else 1f,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessLow,\n            ),\n        label = \"iconScale\",\n    )\n\n    val iconOffsetY by animateDpAsState(\n        targetValue = if (isSelected) (-1).dp else 1.dp,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessLow,\n            ),\n        label = \"iconOffsetY\",\n    )\n\n    val iconTint =\n        if (isSelected) {\n            MaterialTheme.colorScheme.onPrimaryContainer\n        } else {\n            MaterialTheme.colorScheme.onSurface.copy(alpha = .7f)\n        }\n\n    val labelAlpha by animateFloatAsState(\n        targetValue = if (isSelected) 1f else 0f,\n        animationSpec =\n            tween(\n                durationMillis = if (isSelected) 250 else 150,\n                easing = FastOutSlowInEasing,\n            ),\n        label = \"labelAlpha\",\n    )\n\n    val labelScale by animateFloatAsState(\n        targetValue = if (isSelected) 1f else 0.6f,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessLow,\n            ),\n        label = \"labelScale\",\n    )\n\n    val horizontalPadding by animateDpAsState(\n        targetValue = if (isSelected) 20.dp else 14.dp,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioNoBouncy,\n                stiffness = Spring.StiffnessMediumLow,\n            ),\n        label = \"hPadding\",\n    )\n\n    Box(\n        modifier =\n            Modifier\n                .clip(CircleShape)\n                .clickable(\n                    interactionSource = interactionSource,\n                    indication = null,\n                ) { onSelect() }\n                .onGloballyPositioned { coordinates ->\n                    val x = coordinates.positionInParent().x\n                    val width = coordinates.size.width.toFloat()\n                    scope.launch { onPositioned(x, width) }\n                }.graphicsLayer {\n                    scaleX = pressScale\n                    scaleY = pressScale\n                }.padding(horizontal = horizontalPadding, vertical = 6.dp),\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(1.dp),\n        ) {\n            Icon(\n                imageVector = if (isSelected) item.iconFilled else item.iconOutlined,\n                contentDescription = stringResource(item.titleRes),\n                modifier =\n                    Modifier\n                        .size(22.dp)\n                        .graphicsLayer {\n                            scaleX = iconScale\n                            scaleY = iconScale\n                            translationY = with(density) { iconOffsetY.toPx() }\n                        },\n                tint = iconTint,\n            )\n\n            Box(\n                modifier =\n                    Modifier\n                        .height(if (isSelected) 16.dp else 0.dp)\n                        .graphicsLayer {\n                            alpha = labelAlpha\n                            scaleX = labelScale\n                            scaleY = labelScale\n                        },\n                contentAlignment = Alignment.Center,\n            ) {\n                Text(\n                    text = stringResource(item.titleRes),\n                    style =\n                        MaterialTheme.typography.labelSmall.copy(\n                            fontSize = 10.sp,\n                            fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,\n                            lineHeight = 12.sp,\n                        ),\n                    color =\n                        if (isSelected) {\n                            MaterialTheme.colorScheme.onSurface\n                        } else {\n                            MaterialTheme.colorScheme.onSurface.copy(alpha = .7f)\n                        },\n                    maxLines = 1,\n                )\n            }\n        }\n\n        if (hasBadge) {\n            Box(\n                Modifier\n                    .size(12.dp)\n                    .clip(CircleShape)\n                    .background(MaterialTheme.colorScheme.error)\n                    .align(Alignment.TopEnd),\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nfun BottomNavigationPreview() {\n    GithubStoreTheme {\n        CompositionLocalProvider(\n            LocalBottomNavigationLiquid provides rememberLiquidState(),\n        ) {\n            BottomNavigation(\n                currentScreen = GithubStoreGraph.HomeScreen,\n                onNavigate = {\n                },\n                isUpdateAvailable = true,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt",
    "content": "package zed.rainxch.githubstore.app.navigation\n\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Apps\nimport androidx.compose.material.icons.filled.Home\nimport androidx.compose.material.icons.filled.Person2\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.outlined.Apps\nimport androidx.compose.material.icons.outlined.Home\nimport androidx.compose.material.icons.outlined.Person2\nimport androidx.compose.material.icons.outlined.Search\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport org.jetbrains.compose.resources.StringResource\nimport zed.rainxch.core.domain.getPlatform\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.githubstore.core.presentation.res.*\n\ndata class BottomNavigationItem(\n    val titleRes: StringResource,\n    val iconOutlined: ImageVector,\n    val iconFilled: ImageVector,\n    val screen: GithubStoreGraph,\n)\n\nobject BottomNavigationUtils {\n    fun items(): List<BottomNavigationItem> =\n        listOf(\n            BottomNavigationItem(\n                titleRes = Res.string.bottom_nav_home_title,\n                iconOutlined = Icons.Outlined.Home,\n                iconFilled = Icons.Filled.Home,\n                screen = GithubStoreGraph.HomeScreen,\n            ),\n            BottomNavigationItem(\n                titleRes = Res.string.bottom_nav_search_title,\n                iconOutlined = Icons.Outlined.Search,\n                iconFilled = Icons.Filled.Search,\n                screen = GithubStoreGraph.SearchScreen,\n            ),\n            BottomNavigationItem(\n                titleRes = Res.string.bottom_nav_apps_title,\n                iconOutlined = Icons.Outlined.Apps,\n                iconFilled = Icons.Filled.Apps,\n                screen = GithubStoreGraph.AppsScreen,\n            ),\n            BottomNavigationItem(\n                titleRes = Res.string.bottom_nav_profile_title,\n                iconOutlined = Icons.Outlined.Person2,\n                iconFilled = Icons.Filled.Person2,\n                screen = GithubStoreGraph.ProfileScreen,\n            ),\n        )\n\n    fun allowedScreens(): List<GithubStoreGraph> =\n        items()\n            .filterNot {\n                getPlatform() != Platform.ANDROID &&\n                    it.screen == GithubStoreGraph.AppsScreen\n            }.map { it.screen }\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt",
    "content": "package zed.rainxch.githubstore.app.navigation\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed interface GithubStoreGraph {\n    @Serializable\n    data object HomeScreen : GithubStoreGraph\n\n    @Serializable\n    data object SearchScreen : GithubStoreGraph\n\n    @Serializable\n    data object AuthenticationScreen : GithubStoreGraph\n\n    @Serializable\n    data class DetailsScreen(\n        val repositoryId: Long = -1L,\n        val owner: String = \"\",\n        val repo: String = \"\",\n        val isComingFromUpdate: Boolean = false,\n    ) : GithubStoreGraph\n\n    @Serializable\n    data class DeveloperProfileScreen(\n        val username: String,\n    ) : GithubStoreGraph\n\n    @Serializable\n    data object ProfileScreen : GithubStoreGraph\n\n    @Serializable\n    data object FavouritesScreen : GithubStoreGraph\n\n    @Serializable\n    data object StarredReposScreen : GithubStoreGraph\n\n    @Serializable\n    data object AppsScreen : GithubStoreGraph\n\n    @Serializable\n    data object SponsorScreen : GithubStoreGraph\n}\n"
  },
  {
    "path": "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/NavigationUtils.kt",
    "content": "package zed.rainxch.githubstore.app.navigation\n\nimport androidx.navigation.NavBackStackEntry\nimport androidx.navigation.toRoute\n\nfun NavBackStackEntry?.getCurrentScreen(): GithubStoreGraph? {\n    if (this == null) return null\n    val route = destination.route ?: return null\n\n    return when {\n        route.contains(\"HomeScreen\") -> GithubStoreGraph.HomeScreen\n        route.contains(\"SearchScreen\") -> GithubStoreGraph.SearchScreen\n        route.contains(\"AuthenticationScreen\") -> GithubStoreGraph.AuthenticationScreen\n        route.contains(\"DetailsScreen\") -> toRoute<GithubStoreGraph.DetailsScreen>()\n        route.contains(\"DeveloperProfileScreen\") -> toRoute<GithubStoreGraph.DeveloperProfileScreen>()\n        route.contains(\"ProfileScreen\") -> GithubStoreGraph.ProfileScreen\n        route.contains(\"FavouritesScreen\") -> GithubStoreGraph.FavouritesScreen\n        route.contains(\"StarredReposScreen\") -> GithubStoreGraph.StarredReposScreen\n        route.contains(\"AppsScreen\") -> GithubStoreGraph.AppsScreen\n        else -> null\n    }\n}\n"
  },
  {
    "path": "composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt",
    "content": "package zed.rainxch.githubstore\n\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.isCtrlPressed\nimport androidx.compose.ui.input.key.isMetaPressed\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.window.Window\nimport androidx.compose.ui.window.application\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.app.desktop.KeyboardNavigation\nimport zed.rainxch.githubstore.app.desktop.KeyboardNavigationEvent\nimport zed.rainxch.githubstore.app.di.initKoin\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.app_icon\nimport zed.rainxch.githubstore.core.presentation.res.app_name\nimport java.awt.Desktop\nimport kotlin.system.exitProcess\n\nfun main(args: Array<String>) {\n    initKoin()\n\n    val deepLinkArg = args.firstOrNull()\n\n    if (deepLinkArg != null && DesktopDeepLink.tryForwardToRunningInstance(deepLinkArg)) {\n        exitProcess(0)\n    }\n\n    DesktopDeepLink.registerUriSchemeIfNeeded()\n\n    application {\n        var deepLinkUri by mutableStateOf(deepLinkArg)\n\n        LaunchedEffect(Unit) {\n            DesktopDeepLink.startInstanceListener { uri ->\n                deepLinkUri = uri\n            }\n        }\n\n        if (Desktop.isDesktopSupported()) {\n            Desktop.getDesktop().let { desktop ->\n                if (desktop.isSupported(Desktop.Action.APP_OPEN_URI)) {\n                    desktop.setOpenURIHandler { event ->\n                        deepLinkUri = event.uri.toString()\n                    }\n                }\n            }\n        }\n\n        Window(\n            onCloseRequest = ::exitApplication,\n            title = stringResource(Res.string.app_name),\n            icon = painterResource(Res.drawable.app_icon),\n            onKeyEvent = { keyEvent ->\n                if (keyEvent.key == Key.F && keyEvent.type == KeyEventType.KeyDown) {\n                    if (keyEvent.isCtrlPressed || keyEvent.isMetaPressed) {\n                        KeyboardNavigation.onKeyClicked(KeyboardNavigationEvent.OnCtrlFClick)\n                        true\n                    } else {\n                        false\n                    }\n                } else {\n                    false\n                }\n            },\n        ) {\n            App(deepLinkUri = deepLinkUri)\n        }\n    }\n}\n"
  },
  {
    "path": "composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopDeepLink.kt",
    "content": "package zed.rainxch.githubstore\n\nimport java.io.BufferedReader\nimport java.io.File\nimport java.io.InputStreamReader\nimport java.io.PrintWriter\nimport java.net.InetAddress\nimport java.net.ServerSocket\nimport java.net.Socket\n\nobject DesktopDeepLink {\n    private const val SINGLE_INSTANCE_PORT = 47632\n    private const val SCHEME = \"githubstore\"\n    private const val DESKTOP_FILE_NAME = \"github-store-deeplink\"\n\n    /**\n     * On Windows and Linux, ensure the `githubstore://` protocol is registered.\n     * - Windows: Writes to HKCU registry.\n     * - Linux: Creates a `.desktop` file and registers via `xdg-mime`.\n     * No-op on macOS (handled via Info.plist in the packaged .app).\n     */\n    fun registerUriSchemeIfNeeded() {\n        when {\n            isWindows() -> registerWindows()\n            isLinux() -> registerLinux()\n        }\n    }\n\n    private fun registerWindows() {\n        val checkResult =\n            runCommand(\n                \"reg\",\n                \"query\",\n                \"HKCU\\\\SOFTWARE\\\\Classes\\\\$SCHEME\",\n                \"/ve\",\n            )\n        if (checkResult != null && checkResult.contains(\"URL:\")) return\n\n        val exePath = resolveExePath() ?: return\n\n        runCommand(\n            \"reg\",\n            \"add\",\n            \"HKCU\\\\SOFTWARE\\\\Classes\\\\$SCHEME\",\n            \"/ve\",\n            \"/d\",\n            \"URL:GitHub Store Protocol\",\n            \"/f\",\n        )\n        runCommand(\n            \"reg\",\n            \"add\",\n            \"HKCU\\\\SOFTWARE\\\\Classes\\\\$SCHEME\",\n            \"/v\",\n            \"URL Protocol\",\n            \"/d\",\n            \"\",\n            \"/f\",\n        )\n        runCommand(\n            \"reg\",\n            \"add\",\n            \"HKCU\\\\SOFTWARE\\\\Classes\\\\$SCHEME\\\\DefaultIcon\",\n            \"/ve\",\n            \"/d\",\n            \"\\\"$exePath\\\",1\",\n            \"/f\",\n        )\n        runCommand(\n            \"reg\",\n            \"add\",\n            \"HKCU\\\\SOFTWARE\\\\Classes\\\\$SCHEME\\\\shell\\\\open\\\\command\",\n            \"/ve\",\n            \"/d\",\n            \"\\\"$exePath\\\" \\\"%1\\\"\",\n            \"/f\",\n        )\n    }\n\n    private fun registerLinux() {\n        val appsDir = File(System.getProperty(\"user.home\"), \".local/share/applications\")\n        val desktopFile = File(appsDir, \"$DESKTOP_FILE_NAME.desktop\")\n\n        if (desktopFile.exists()) return\n\n        val exePath = resolveExePath() ?: return\n\n        appsDir.mkdirs()\n\n        desktopFile.writeText(\n            \"\"\"\n            [Desktop Entry]\n            Type=Application\n            Name=GitHub Store\n            Exec=\"$exePath\" %u\n            Terminal=false\n            MimeType=x-scheme-handler/$SCHEME;\n            NoDisplay=true\n            \"\"\".trimIndent(),\n        )\n\n        runCommand(\"xdg-mime\", \"default\", \"$DESKTOP_FILE_NAME.desktop\", \"x-scheme-handler/$SCHEME\")\n    }\n\n    /**\n     * Try to forward a deep link URI to an already-running instance.\n     * @return `true` if the URI was forwarded (this instance should exit),\n     *         `false` if no existing instance is running.\n     */\n    fun tryForwardToRunningInstance(uri: String): Boolean =\n        try {\n            Socket(\"127.0.0.1\", SINGLE_INSTANCE_PORT).use { socket ->\n                PrintWriter(socket.getOutputStream(), true).println(uri)\n            }\n            true\n        } catch (_: Exception) {\n            false\n        }\n\n    /**\n     * Start listening for URIs forwarded from new instances.\n     * Calls [onUri] on the main thread when a URI is received.\n     */\n    fun startInstanceListener(onUri: (String) -> Unit) {\n        val thread =\n            Thread({\n                try {\n                    val server = ServerSocket(SINGLE_INSTANCE_PORT, 50, InetAddress.getLoopbackAddress())\n                    while (true) {\n                        val client = server.accept()\n                        try {\n                            val reader = BufferedReader(InputStreamReader(client.getInputStream()))\n                            val uri = reader.readLine()\n                            if (!uri.isNullOrBlank()) {\n                                onUri(uri.trim())\n                            }\n                        } catch (_: Exception) {\n                        } finally {\n                            client.close()\n                        }\n                    }\n                } catch (_: Exception) {\n                }\n            }, \"DeepLinkListener\")\n        thread.isDaemon = true\n        thread.start()\n    }\n\n    private fun isWindows(): Boolean = System.getProperty(\"os.name\")?.lowercase()?.contains(\"win\") == true\n\n    private fun isLinux(): Boolean = System.getProperty(\"os.name\")?.lowercase()?.contains(\"linux\") == true\n\n    private fun resolveExePath(): String? =\n        try {\n            ProcessHandle\n                .current()\n                .info()\n                .command()\n                .orElse(null)\n        } catch (_: Exception) {\n            null\n        }\n\n    private fun runCommand(vararg cmd: String): String? =\n        try {\n            val process =\n                ProcessBuilder(*cmd)\n                    .redirectErrorStream(true)\n                    .start()\n            val output = process.inputStream.bufferedReader().readText()\n            process.waitFor()\n            output\n        } catch (_: Exception) {\n            null\n        }\n}\n"
  },
  {
    "path": "core/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "core/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.room)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nandroid {\n    buildFeatures {\n        aidl = true\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n\n                implementation(libs.touchlab.kermit)\n\n                implementation(libs.datastore)\n                implementation(libs.datastore.preferences)\n\n                implementation(libs.kotlinx.datetime)\n            }\n        }\n\n        androidMain {\n            dependencies {\n                implementation(libs.ktor.client.okhttp)\n                implementation(libs.androidx.work.runtime)\n                implementation(libs.shizuku.api)\n                implementation(libs.shizuku.provider)\n                compileOnly(libs.hidden.api.stub)\n            }\n        }\n\n        jvmMain {\n            dependencies {\n                implementation(libs.ktor.client.okhttp)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"81a8b0bb930a5c839e0da7a5335e4991\",\n    \"entities\": [\n      {\n        \"tableName\": \"installed_apps\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `installedVersion` TEXT NOT NULL, `installedAssetName` TEXT, `installedAssetUrl` TEXT, `latestVersion` TEXT, `latestAssetName` TEXT, `latestAssetUrl` TEXT, `latestAssetSize` INTEGER, `appName` TEXT NOT NULL, `installSource` TEXT NOT NULL, `installedAt` INTEGER NOT NULL, `lastCheckedAt` INTEGER NOT NULL, `lastUpdatedAt` INTEGER NOT NULL, `isUpdateAvailable` INTEGER NOT NULL, `updateCheckEnabled` INTEGER NOT NULL, `releaseNotes` TEXT, `systemArchitecture` TEXT NOT NULL, `fileExtension` TEXT NOT NULL, `isPendingInstall` INTEGER NOT NULL, `installedVersionName` TEXT, `installedVersionCode` INTEGER NOT NULL, `latestVersionName` TEXT, `latestVersionCode` INTEGER, PRIMARY KEY(`packageName`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersion\",\n            \"columnName\": \"installedVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedAssetName\",\n            \"columnName\": \"installedAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedAssetUrl\",\n            \"columnName\": \"installedAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetName\",\n            \"columnName\": \"latestAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetUrl\",\n            \"columnName\": \"latestAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetSize\",\n            \"columnName\": \"latestAssetSize\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installSource\",\n            \"columnName\": \"installSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedAt\",\n            \"columnName\": \"installedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastCheckedAt\",\n            \"columnName\": \"lastCheckedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdatedAt\",\n            \"columnName\": \"lastUpdatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUpdateAvailable\",\n            \"columnName\": \"isUpdateAvailable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateCheckEnabled\",\n            \"columnName\": \"updateCheckEnabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"releaseNotes\",\n            \"columnName\": \"releaseNotes\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"systemArchitecture\",\n            \"columnName\": \"systemArchitecture\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fileExtension\",\n            \"columnName\": \"fileExtension\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isPendingInstall\",\n            \"columnName\": \"isPendingInstall\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersionName\",\n            \"columnName\": \"installedVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedVersionCode\",\n            \"columnName\": \"installedVersionCode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"latestVersionName\",\n            \"columnName\": \"latestVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersionCode\",\n            \"columnName\": \"latestVersionCode\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"packageName\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"favorite_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"update_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `appName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoName` TEXT NOT NULL, `fromVersion` TEXT NOT NULL, `toVersion` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `updateSource` TEXT NOT NULL, `success` INTEGER NOT NULL, `errorMessage` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fromVersion\",\n            \"columnName\": \"fromVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"toVersion\",\n            \"columnName\": \"toVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateSource\",\n            \"columnName\": \"updateSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"success\",\n            \"columnName\": \"success\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"errorMessage\",\n            \"columnName\": \"errorMessage\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"starred_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `stargazersCount` INTEGER NOT NULL, `forksCount` INTEGER NOT NULL, `openIssuesCount` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `starredAt` INTEGER, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"stargazersCount\",\n            \"columnName\": \"stargazersCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"forksCount\",\n            \"columnName\": \"forksCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"openIssuesCount\",\n            \"columnName\": \"openIssuesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"starredAt\",\n            \"columnName\": \"starredAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '81a8b0bb930a5c839e0da7a5335e4991')\"\n    ]\n  }\n}"
  },
  {
    "path": "core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/4.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 4,\n    \"identityHash\": \"7d8cd14625a6c8b570092980957d57ce\",\n    \"entities\": [\n      {\n        \"tableName\": \"installed_apps\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `installedVersion` TEXT NOT NULL, `installedAssetName` TEXT, `installedAssetUrl` TEXT, `latestVersion` TEXT, `latestAssetName` TEXT, `latestAssetUrl` TEXT, `latestAssetSize` INTEGER, `appName` TEXT NOT NULL, `installSource` TEXT NOT NULL, `installedAt` INTEGER NOT NULL, `lastCheckedAt` INTEGER NOT NULL, `lastUpdatedAt` INTEGER NOT NULL, `isUpdateAvailable` INTEGER NOT NULL, `updateCheckEnabled` INTEGER NOT NULL, `releaseNotes` TEXT, `systemArchitecture` TEXT NOT NULL, `fileExtension` TEXT NOT NULL, `isPendingInstall` INTEGER NOT NULL, `installedVersionName` TEXT, `installedVersionCode` INTEGER NOT NULL, `latestVersionName` TEXT, `latestVersionCode` INTEGER, PRIMARY KEY(`packageName`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersion\",\n            \"columnName\": \"installedVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedAssetName\",\n            \"columnName\": \"installedAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedAssetUrl\",\n            \"columnName\": \"installedAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetName\",\n            \"columnName\": \"latestAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetUrl\",\n            \"columnName\": \"latestAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetSize\",\n            \"columnName\": \"latestAssetSize\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installSource\",\n            \"columnName\": \"installSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedAt\",\n            \"columnName\": \"installedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastCheckedAt\",\n            \"columnName\": \"lastCheckedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdatedAt\",\n            \"columnName\": \"lastUpdatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUpdateAvailable\",\n            \"columnName\": \"isUpdateAvailable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateCheckEnabled\",\n            \"columnName\": \"updateCheckEnabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"releaseNotes\",\n            \"columnName\": \"releaseNotes\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"systemArchitecture\",\n            \"columnName\": \"systemArchitecture\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fileExtension\",\n            \"columnName\": \"fileExtension\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isPendingInstall\",\n            \"columnName\": \"isPendingInstall\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersionName\",\n            \"columnName\": \"installedVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedVersionCode\",\n            \"columnName\": \"installedVersionCode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"latestVersionName\",\n            \"columnName\": \"latestVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersionCode\",\n            \"columnName\": \"latestVersionCode\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"packageName\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"favorite_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"update_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `appName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoName` TEXT NOT NULL, `fromVersion` TEXT NOT NULL, `toVersion` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `updateSource` TEXT NOT NULL, `success` INTEGER NOT NULL, `errorMessage` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fromVersion\",\n            \"columnName\": \"fromVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"toVersion\",\n            \"columnName\": \"toVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateSource\",\n            \"columnName\": \"updateSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"success\",\n            \"columnName\": \"success\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"errorMessage\",\n            \"columnName\": \"errorMessage\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"starred_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `stargazersCount` INTEGER NOT NULL, `forksCount` INTEGER NOT NULL, `openIssuesCount` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `starredAt` INTEGER, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"stargazersCount\",\n            \"columnName\": \"stargazersCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"forksCount\",\n            \"columnName\": \"forksCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"openIssuesCount\",\n            \"columnName\": \"openIssuesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"starredAt\",\n            \"columnName\": \"starredAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"cache_entries\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `jsonData` TEXT NOT NULL, `cachedAt` INTEGER NOT NULL, `expiresAt` INTEGER NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"jsonData\",\n            \"columnName\": \"jsonData\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cachedAt\",\n            \"columnName\": \"cachedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"expiresAt\",\n            \"columnName\": \"expiresAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"key\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7d8cd14625a6c8b570092980957d57ce')\"\n    ]\n  }\n}"
  },
  {
    "path": "core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/5.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 5,\n    \"identityHash\": \"9d5111af6d583511c868ab59557e4ea8\",\n    \"entities\": [\n      {\n        \"tableName\": \"installed_apps\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `installedVersion` TEXT NOT NULL, `installedAssetName` TEXT, `installedAssetUrl` TEXT, `latestVersion` TEXT, `latestAssetName` TEXT, `latestAssetUrl` TEXT, `latestAssetSize` INTEGER, `appName` TEXT NOT NULL, `installSource` TEXT NOT NULL, `signingFingerprint` TEXT, `installedAt` INTEGER NOT NULL, `lastCheckedAt` INTEGER NOT NULL, `lastUpdatedAt` INTEGER NOT NULL, `isUpdateAvailable` INTEGER NOT NULL, `updateCheckEnabled` INTEGER NOT NULL, `releaseNotes` TEXT, `systemArchitecture` TEXT NOT NULL, `fileExtension` TEXT NOT NULL, `isPendingInstall` INTEGER NOT NULL, `installedVersionName` TEXT, `installedVersionCode` INTEGER NOT NULL, `latestVersionName` TEXT, `latestVersionCode` INTEGER, PRIMARY KEY(`packageName`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersion\",\n            \"columnName\": \"installedVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedAssetName\",\n            \"columnName\": \"installedAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedAssetUrl\",\n            \"columnName\": \"installedAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetName\",\n            \"columnName\": \"latestAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetUrl\",\n            \"columnName\": \"latestAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetSize\",\n            \"columnName\": \"latestAssetSize\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installSource\",\n            \"columnName\": \"installSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"signingFingerprint\",\n            \"columnName\": \"signingFingerprint\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedAt\",\n            \"columnName\": \"installedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastCheckedAt\",\n            \"columnName\": \"lastCheckedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdatedAt\",\n            \"columnName\": \"lastUpdatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUpdateAvailable\",\n            \"columnName\": \"isUpdateAvailable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateCheckEnabled\",\n            \"columnName\": \"updateCheckEnabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"releaseNotes\",\n            \"columnName\": \"releaseNotes\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"systemArchitecture\",\n            \"columnName\": \"systemArchitecture\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fileExtension\",\n            \"columnName\": \"fileExtension\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isPendingInstall\",\n            \"columnName\": \"isPendingInstall\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersionName\",\n            \"columnName\": \"installedVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedVersionCode\",\n            \"columnName\": \"installedVersionCode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"latestVersionName\",\n            \"columnName\": \"latestVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersionCode\",\n            \"columnName\": \"latestVersionCode\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"packageName\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"favorite_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"update_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `appName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoName` TEXT NOT NULL, `fromVersion` TEXT NOT NULL, `toVersion` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `updateSource` TEXT NOT NULL, `success` INTEGER NOT NULL, `errorMessage` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fromVersion\",\n            \"columnName\": \"fromVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"toVersion\",\n            \"columnName\": \"toVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateSource\",\n            \"columnName\": \"updateSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"success\",\n            \"columnName\": \"success\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"errorMessage\",\n            \"columnName\": \"errorMessage\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"starred_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `stargazersCount` INTEGER NOT NULL, `forksCount` INTEGER NOT NULL, `openIssuesCount` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `starredAt` INTEGER, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"stargazersCount\",\n            \"columnName\": \"stargazersCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"forksCount\",\n            \"columnName\": \"forksCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"openIssuesCount\",\n            \"columnName\": \"openIssuesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"starredAt\",\n            \"columnName\": \"starredAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"cache_entries\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `jsonData` TEXT NOT NULL, `cachedAt` INTEGER NOT NULL, `expiresAt` INTEGER NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"jsonData\",\n            \"columnName\": \"jsonData\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cachedAt\",\n            \"columnName\": \"cachedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"expiresAt\",\n            \"columnName\": \"expiresAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"key\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9d5111af6d583511c868ab59557e4ea8')\"\n    ]\n  }\n}"
  },
  {
    "path": "core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/6.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 6,\n    \"identityHash\": \"ce64b232de8d6ab9d95e3adabf7c7c43\",\n    \"entities\": [\n      {\n        \"tableName\": \"installed_apps\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `installedVersion` TEXT NOT NULL, `installedAssetName` TEXT, `installedAssetUrl` TEXT, `latestVersion` TEXT, `latestAssetName` TEXT, `latestAssetUrl` TEXT, `latestAssetSize` INTEGER, `appName` TEXT NOT NULL, `installSource` TEXT NOT NULL, `signingFingerprint` TEXT, `installedAt` INTEGER NOT NULL, `lastCheckedAt` INTEGER NOT NULL, `lastUpdatedAt` INTEGER NOT NULL, `isUpdateAvailable` INTEGER NOT NULL, `updateCheckEnabled` INTEGER NOT NULL, `releaseNotes` TEXT, `systemArchitecture` TEXT NOT NULL, `fileExtension` TEXT NOT NULL, `isPendingInstall` INTEGER NOT NULL, `installedVersionName` TEXT, `installedVersionCode` INTEGER NOT NULL, `latestVersionName` TEXT, `latestVersionCode` INTEGER, PRIMARY KEY(`packageName`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersion\",\n            \"columnName\": \"installedVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedAssetName\",\n            \"columnName\": \"installedAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedAssetUrl\",\n            \"columnName\": \"installedAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetName\",\n            \"columnName\": \"latestAssetName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetUrl\",\n            \"columnName\": \"latestAssetUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestAssetSize\",\n            \"columnName\": \"latestAssetSize\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installSource\",\n            \"columnName\": \"installSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"signingFingerprint\",\n            \"columnName\": \"signingFingerprint\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedAt\",\n            \"columnName\": \"installedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastCheckedAt\",\n            \"columnName\": \"lastCheckedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdatedAt\",\n            \"columnName\": \"lastUpdatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUpdateAvailable\",\n            \"columnName\": \"isUpdateAvailable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateCheckEnabled\",\n            \"columnName\": \"updateCheckEnabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"releaseNotes\",\n            \"columnName\": \"releaseNotes\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"systemArchitecture\",\n            \"columnName\": \"systemArchitecture\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fileExtension\",\n            \"columnName\": \"fileExtension\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isPendingInstall\",\n            \"columnName\": \"isPendingInstall\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedVersionName\",\n            \"columnName\": \"installedVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"installedVersionCode\",\n            \"columnName\": \"installedVersionCode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"latestVersionName\",\n            \"columnName\": \"latestVersionName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersionCode\",\n            \"columnName\": \"latestVersionCode\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"packageName\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"favorite_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"update_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `appName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoName` TEXT NOT NULL, `fromVersion` TEXT NOT NULL, `toVersion` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `updateSource` TEXT NOT NULL, `success` INTEGER NOT NULL, `errorMessage` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"appName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"fromVersion\",\n            \"columnName\": \"fromVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"toVersion\",\n            \"columnName\": \"toVersion\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updatedAt\",\n            \"columnName\": \"updatedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateSource\",\n            \"columnName\": \"updateSource\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"success\",\n            \"columnName\": \"success\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"errorMessage\",\n            \"columnName\": \"errorMessage\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"starred_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `stargazersCount` INTEGER NOT NULL, `forksCount` INTEGER NOT NULL, `openIssuesCount` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `starredAt` INTEGER, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoName\",\n            \"columnName\": \"repoName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwner\",\n            \"columnName\": \"repoOwner\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoOwnerAvatarUrl\",\n            \"columnName\": \"repoOwnerAvatarUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"repoDescription\",\n            \"columnName\": \"repoDescription\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"primaryLanguage\",\n            \"columnName\": \"primaryLanguage\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"repoUrl\",\n            \"columnName\": \"repoUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"stargazersCount\",\n            \"columnName\": \"stargazersCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"forksCount\",\n            \"columnName\": \"forksCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"openIssuesCount\",\n            \"columnName\": \"openIssuesCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isInstalled\",\n            \"columnName\": \"isInstalled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"installedPackageName\",\n            \"columnName\": \"installedPackageName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestVersion\",\n            \"columnName\": \"latestVersion\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"latestReleaseUrl\",\n            \"columnName\": \"latestReleaseUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"starredAt\",\n            \"columnName\": \"starredAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"addedAt\",\n            \"columnName\": \"addedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastSyncedAt\",\n            \"columnName\": \"lastSyncedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"cache_entries\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `jsonData` TEXT NOT NULL, `cachedAt` INTEGER NOT NULL, `expiresAt` INTEGER NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"jsonData\",\n            \"columnName\": \"jsonData\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cachedAt\",\n            \"columnName\": \"cachedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"expiresAt\",\n            \"columnName\": \"expiresAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"key\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"seen_repos\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `seenAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"repoId\",\n            \"columnName\": \"repoId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"seenAt\",\n            \"columnName\": \"seenAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"repoId\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ce64b232de8d6ab9d95e3adabf7c7c43')\"\n    ]\n  }\n}"
  },
  {
    "path": "core/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "core/data/src/androidMain/aidl/zed/rainxch/core/data/services/shizuku/IShizukuInstallerService.aidl",
    "content": "package zed.rainxch.core.data.services.shizuku;\n\ninterface IShizukuInstallerService {\n    int installPackage(in ParcelFileDescriptor pfd, long fileSize);\n    int uninstallPackage(String packageName);\n    void destroy();\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt",
    "content": "package zed.rainxch.core.data.di\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport kotlinx.coroutines.CoroutineScope\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.dsl.module\nimport zed.rainxch.core.data.local.data_store.createDataStore\nimport zed.rainxch.core.data.local.db.AppDatabase\nimport zed.rainxch.core.data.local.db.initDatabase\nimport zed.rainxch.core.data.services.AndroidDownloader\nimport zed.rainxch.core.data.services.AndroidFileLocationsProvider\nimport zed.rainxch.core.data.services.AndroidInstaller\nimport zed.rainxch.core.data.services.AndroidInstallerInfoExtractor\nimport zed.rainxch.core.data.services.AndroidLocalizationManager\nimport zed.rainxch.core.data.services.AndroidPackageMonitor\nimport zed.rainxch.core.data.services.AndroidUpdateScheduleManager\nimport zed.rainxch.core.data.services.FileLocationsProvider\nimport zed.rainxch.core.data.services.LocalizationManager\nimport zed.rainxch.core.data.services.shizuku.AndroidInstallerStatusProvider\nimport zed.rainxch.core.data.services.shizuku.ShizukuInstallerWrapper\nimport zed.rainxch.core.data.services.shizuku.ShizukuServiceManager\nimport zed.rainxch.core.data.utils.AndroidAppLauncher\nimport zed.rainxch.core.data.utils.AndroidBrowserHelper\nimport zed.rainxch.core.data.utils.AndroidClipboardHelper\nimport zed.rainxch.core.data.utils.AndroidShareManager\nimport zed.rainxch.core.domain.network.Downloader\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.system.InstallerStatusProvider\nimport zed.rainxch.core.domain.system.PackageMonitor\nimport zed.rainxch.core.domain.system.UpdateScheduleManager\nimport zed.rainxch.core.domain.utils.AppLauncher\nimport zed.rainxch.core.domain.utils.BrowserHelper\nimport zed.rainxch.core.domain.utils.ClipboardHelper\nimport zed.rainxch.core.domain.utils.ShareManager\n\nactual val corePlatformModule =\n    module {\n        // Core\n\n        single<Downloader> {\n            AndroidDownloader(\n                files = get(),\n            )\n        }\n\n        // AndroidInstaller — registered by class so the wrapper can inject it\n        single {\n            AndroidInstaller(\n                context = get(),\n                installerInfoExtractor = AndroidInstallerInfoExtractor(androidContext()),\n            )\n        }\n\n        // ShizukuServiceManager — manages Shizuku lifecycle, permissions, service binding\n        single {\n            ShizukuServiceManager(\n                context = androidContext(),\n            ).also { it.initialize() }\n        }\n\n        // Installer — the ShizukuInstallerWrapper is the public Installer singleton.\n        // It delegates to AndroidInstaller by default, intercepting with Shizuku when enabled.\n        single<Installer> {\n            ShizukuInstallerWrapper(\n                androidInstaller = get<AndroidInstaller>(),\n                shizukuServiceManager = get(),\n                tweaksRepository = get(),\n            ).also { wrapper ->\n                wrapper.observeInstallerPreference(get<CoroutineScope>())\n            }\n        }\n\n        // InstallerStatusProvider — exposes Shizuku availability to the UI layer\n        single<InstallerStatusProvider> {\n            AndroidInstallerStatusProvider(\n                shizukuServiceManager = get(),\n                scope = get(),\n            )\n        }\n\n        single<FileLocationsProvider> {\n            AndroidFileLocationsProvider(context = get())\n        }\n\n        single<PackageMonitor> {\n            AndroidPackageMonitor(androidContext())\n        }\n\n        single<LocalizationManager> {\n            AndroidLocalizationManager()\n        }\n\n        // Locals\n\n        single<AppDatabase> {\n            initDatabase(androidContext())\n        }\n\n        single<DataStore<Preferences>> {\n            createDataStore(androidContext())\n        }\n\n        // Utils\n\n        single<BrowserHelper> {\n            AndroidBrowserHelper(androidContext())\n        }\n\n        single<ClipboardHelper> {\n            AndroidClipboardHelper(androidContext())\n        }\n\n        single<AppLauncher> {\n            AndroidAppLauncher(\n                context = androidContext(),\n                logger = get(),\n            )\n        }\n\n        single<ShareManager> {\n            AndroidShareManager(\n                context = androidContext(),\n            )\n        }\n\n        single<UpdateScheduleManager> {\n            AndroidUpdateScheduleManager(\n                context = androidContext(),\n            )\n        }\n    }\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/data_store/createDataStore.kt",
    "content": "package zed.rainxch.core.data.local.data_store\n\nimport android.content.Context\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\n\nfun createDataStore(context: Context): DataStore<Preferences> =\n    createDataStore(\n        producePath = {\n            context.filesDir.resolve(_root_ide_package_.zed.rainxch.core.data.local.data_store.dataStoreFileName).absolutePath\n        },\n    )\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt",
    "content": "package zed.rainxch.core.data.local.db\n\nimport android.content.Context\nimport androidx.room.Room\nimport kotlinx.coroutines.Dispatchers\nimport zed.rainxch.core.data.local.db.migrations.MIGRATION_1_2\nimport zed.rainxch.core.data.local.db.migrations.MIGRATION_2_3\nimport zed.rainxch.core.data.local.db.migrations.MIGRATION_3_4\nimport zed.rainxch.core.data.local.db.migrations.MIGRATION_4_5\nimport zed.rainxch.core.data.local.db.migrations.MIGRATION_5_6\n\nfun initDatabase(context: Context): AppDatabase {\n    val appContext = context.applicationContext\n    val dbFile = appContext.getDatabasePath(\"github_store.db\")\n    return Room\n        .databaseBuilder<AppDatabase>(\n            context = appContext,\n            name = dbFile.absolutePath,\n        ).setQueryCoroutineContext(Dispatchers.IO)\n        .addMigrations(\n            MIGRATION_1_2,\n            MIGRATION_2_3,\n            MIGRATION_3_4,\n            MIGRATION_4_5,\n            MIGRATION_5_6,\n        ).build()\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_1_2.kt",
    "content": "package zed.rainxch.core.data.local.db.migrations\n\nimport androidx.room.migration.Migration\nimport androidx.sqlite.db.SupportSQLiteDatabase\n\nval MIGRATION_1_2 =\n    object : Migration(1, 2) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            db.execSQL(\"ALTER TABLE installed_apps ADD COLUMN installedVersionName TEXT\")\n            db.execSQL(\"ALTER TABLE installed_apps ADD COLUMN installedVersionCode INTEGER NOT NULL DEFAULT 0\")\n            db.execSQL(\"ALTER TABLE installed_apps ADD COLUMN latestVersionName TEXT\")\n            db.execSQL(\"ALTER TABLE installed_apps ADD COLUMN latestVersionCode INTEGER\")\n        }\n    }\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_2_3.kt",
    "content": "package zed.rainxch.core.data.local.db.migrations\n\nimport androidx.room.migration.Migration\nimport androidx.sqlite.db.SupportSQLiteDatabase\n\nval MIGRATION_2_3 =\n    object : Migration(2, 3) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            db.execSQL(\n                \"\"\"\n                CREATE TABLE starred_repos (\n                    repoId INTEGER NOT NULL,\n                    repoName TEXT NOT NULL,\n                    repoOwner TEXT NOT NULL,\n                    repoOwnerAvatarUrl TEXT NOT NULL,\n                    repoDescription TEXT,\n                    primaryLanguage TEXT,\n                    repoUrl TEXT NOT NULL,\n\n                    stargazersCount INTEGER NOT NULL,\n                    forksCount INTEGER NOT NULL,\n                    openIssuesCount INTEGER NOT NULL,\n\n                    isInstalled INTEGER NOT NULL DEFAULT 0,\n                    installedPackageName TEXT,\n\n                    latestVersion TEXT,\n                    latestReleaseUrl TEXT,\n\n                    starredAt INTEGER,\n                    addedAt INTEGER NOT NULL,\n                    lastSyncedAt INTEGER NOT NULL,\n\n                    PRIMARY KEY(repoId)\n                )\n                \"\"\".trimIndent(),\n            )\n        }\n    }\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_3_4.kt",
    "content": "package zed.rainxch.core.data.local.db.migrations\n\nimport androidx.room.migration.Migration\nimport androidx.sqlite.db.SupportSQLiteDatabase\n\nval MIGRATION_3_4 =\n    object : Migration(3, 4) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            db.execSQL(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS cache_entries (\n                    `key` TEXT NOT NULL,\n                    jsonData TEXT NOT NULL,\n                    cachedAt INTEGER NOT NULL,\n                    expiresAt INTEGER NOT NULL,\n                    PRIMARY KEY(`key`)\n                )\n                \"\"\".trimIndent(),\n            )\n        }\n    }\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_4_5.kt",
    "content": "package zed.rainxch.core.data.local.db.migrations\n\nimport androidx.room.migration.Migration\nimport androidx.sqlite.db.SupportSQLiteDatabase\n\nval MIGRATION_4_5 =\n    object : Migration(4, 5) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            db.execSQL(\"ALTER TABLE installed_apps ADD COLUMN signingFingerprint TEXT\")\n        }\n    }\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_5_6.kt",
    "content": "package zed.rainxch.core.data.local.db.migrations\n\nimport androidx.room.migration.Migration\nimport androidx.sqlite.db.SupportSQLiteDatabase\n\nval MIGRATION_5_6 =\n    object : Migration(5, 6) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            db.execSQL(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS seen_repos (\n                    repoId INTEGER NOT NULL PRIMARY KEY,\n                    seenAt INTEGER NOT NULL\n                )\n                \"\"\".trimIndent(),\n            )\n        }\n    }\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt",
    "content": "package zed.rainxch.core.data.network\n\nimport io.ktor.client.*\nimport io.ktor.client.engine.okhttp.*\nimport okhttp3.Credentials\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport java.net.Authenticator\nimport java.net.InetSocketAddress\nimport java.net.PasswordAuthentication\nimport java.net.Proxy\nimport java.net.ProxySelector\n\nactual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient {\n    Authenticator.setDefault(null)\n\n    return HttpClient(OkHttp) {\n        engine {\n            when (proxyConfig) {\n                is ProxyConfig.None -> {\n                    proxy = Proxy.NO_PROXY\n                }\n\n                is ProxyConfig.System -> {\n                    config {\n                        proxySelector(ProxySelector.getDefault())\n                    }\n                }\n\n                is ProxyConfig.Http -> {\n                    proxy =\n                        Proxy(\n                            Proxy.Type.HTTP,\n                            InetSocketAddress(proxyConfig.host, proxyConfig.port),\n                        )\n                    if (proxyConfig.username != null) {\n                        config {\n                            proxyAuthenticator { _, response ->\n                                response.request\n                                    .newBuilder()\n                                    .header(\n                                        \"Proxy-Authorization\",\n                                        Credentials.basic(\n                                            proxyConfig.username!!,\n                                            proxyConfig.password.orEmpty(),\n                                        ),\n                                    ).build()\n                            }\n                        }\n                    }\n                }\n\n                is ProxyConfig.Socks -> {\n                    proxy =\n                        Proxy(\n                            Proxy.Type.SOCKS,\n                            InetSocketAddress(proxyConfig.host, proxyConfig.port),\n                        )\n\n                    if (proxyConfig.username != null) {\n                        Authenticator.setDefault(\n                            object : Authenticator() {\n                                override fun getPasswordAuthentication(): PasswordAuthentication? {\n                                    if (requestingHost == proxyConfig.host &&\n                                        requestingPort == proxyConfig.port\n                                    ) {\n                                        return PasswordAuthentication(\n                                            proxyConfig.username,\n                                            proxyConfig.password.orEmpty().toCharArray(),\n                                        )\n                                    }\n                                    return null\n                                }\n                            },\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.withContext\nimport okhttp3.Call\nimport okhttp3.Credentials\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport zed.rainxch.core.data.network.ProxyManager\nimport zed.rainxch.core.domain.model.DownloadProgress\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.network.Downloader\nimport java.io.File\nimport java.net.Authenticator\nimport java.net.InetSocketAddress\nimport java.net.PasswordAuthentication\nimport java.net.Proxy\nimport java.util.UUID\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.TimeUnit\n\nclass AndroidDownloader(\n    private val files: FileLocationsProvider,\n    private val proxyManager: ProxyManager = ProxyManager,\n) : Downloader {\n    private val activeDownloads = ConcurrentHashMap<String, Call>()\n    private val activeFileNames = ConcurrentHashMap<String, String>()\n\n    private fun buildClient(): OkHttpClient {\n        Authenticator.setDefault(null)\n\n        return OkHttpClient\n            .Builder()\n            .connectTimeout(30, TimeUnit.SECONDS)\n            .readTimeout(60, TimeUnit.SECONDS)\n            .writeTimeout(60, TimeUnit.SECONDS)\n            .apply {\n                when (val config = proxyManager.currentProxyConfig.value) {\n                    is ProxyConfig.None -> {\n                        proxy(Proxy.NO_PROXY)\n                    }\n\n                    is ProxyConfig.System -> {}\n\n                    is ProxyConfig.Http -> {\n                        proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(config.host, config.port)))\n                        if (config.username != null && config.password != null) {\n                            proxyAuthenticator { _, response ->\n                                response.request\n                                    .newBuilder()\n                                    .header(\n                                        \"Proxy-Authorization\",\n                                        Credentials.basic(config.username!!, config.password!!),\n                                    ).build()\n                            }\n                        }\n                    }\n\n                    is ProxyConfig.Socks -> {\n                        proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(config.host, config.port)))\n                        if (config.username != null && config.password != null) {\n                            Authenticator.setDefault(\n                                object : Authenticator() {\n                                    override fun getPasswordAuthentication() =\n                                        PasswordAuthentication(\n                                            config.username,\n                                            config.password!!.toCharArray(),\n                                        )\n                                },\n                            )\n                        }\n                    }\n                }\n            }.build()\n    }\n\n    override fun download(\n        url: String,\n        suggestedFileName: String?,\n    ): Flow<DownloadProgress> =\n        flow {\n            val client = buildClient()\n\n            val dirPath = files.appDownloadsDir()\n            val dir = File(dirPath)\n            if (!dir.exists()) dir.mkdirs()\n\n            val rawName =\n                suggestedFileName?.takeIf { it.isNotBlank() }\n                    ?: url\n                        .substringAfterLast('/')\n                        .substringBefore('?')\n                        .substringBefore('#')\n                        .ifBlank { \"asset-${UUID.randomUUID()}.apk\" }\n            val safeName = rawName.substringAfterLast('/').substringAfterLast('\\\\')\n            require(safeName.isNotBlank() && safeName != \".\" && safeName != \"..\") {\n                \"Invalid file name: $rawName\"\n            }\n\n            check(!activeFileNames.containsKey(safeName)) {\n                \"A download for '$safeName' is already in progress\"\n            }\n\n            val downloadId = UUID.randomUUID().toString()\n\n            val destination = File(dir, safeName)\n            if (destination.exists()) {\n                Logger.d { \"Deleting existing file before download: ${destination.absolutePath}\" }\n                destination.delete()\n            }\n\n            Logger.d { \"Starting download: $url (id=$downloadId)\" }\n\n            val request = Request.Builder().url(url).build()\n            val call = client.newCall(request)\n\n            activeDownloads[downloadId] = call\n            activeFileNames[safeName] = downloadId\n\n            try {\n                call.execute().use { response ->\n                    if (!response.isSuccessful) {\n                        throw kotlinx.io.IOException(\"Unexpected code ${response.code}\")\n                    }\n\n                    val body = response.body\n                    val contentLength = body.contentLength()\n                    val total = if (contentLength > 0) contentLength else null\n\n                    body.byteStream().use { input ->\n                        destination.outputStream().use { output ->\n                            val buffer = ByteArray(8192)\n                            var downloaded: Long = 0\n                            var bytesRead: Int\n                            while (input.read(buffer).also { bytesRead = it } != -1) {\n                                output.write(buffer, 0, bytesRead)\n                                downloaded += bytesRead\n                                val percent =\n                                    if (total != null) ((downloaded * 100L) / total).toInt() else null\n                                emit(DownloadProgress(downloaded, total, percent))\n                            }\n                        }\n                    }\n\n                    if (destination.exists() && destination.length() > 0) {\n                        Logger.d { \"Download complete: ${destination.absolutePath}\" }\n                        val finalDownloaded = destination.length()\n                        val finalPercent =\n                            if (total != null) ((finalDownloaded * 100L) / total).toInt() else 100\n                        emit(DownloadProgress(finalDownloaded, total, finalPercent))\n                    } else {\n                        throw IllegalStateException(\"File not ready after download: ${destination.absolutePath}\")\n                    }\n                }\n            } catch (e: Exception) {\n                destination.delete()\n                Logger.e(e) { \"Download failed\" }\n                throw e\n            } finally {\n                activeDownloads.remove(downloadId)\n                activeFileNames.remove(safeName)\n            }\n        }.flowOn(Dispatchers.IO)\n\n    override suspend fun saveToFile(\n        url: String,\n        suggestedFileName: String?,\n    ): String =\n        withContext(Dispatchers.IO) {\n            val rawName =\n                suggestedFileName?.takeIf { it.isNotBlank() }\n                    ?: url\n                        .substringAfterLast('/')\n                        .substringBefore('?')\n                        .substringBefore('#')\n                        .ifBlank { \"asset-${UUID.randomUUID()}.apk\" }\n            val safeName = rawName.substringAfterLast('/').substringAfterLast('\\\\')\n            require(safeName.isNotBlank() && safeName != \".\" && safeName != \"..\") {\n                \"Invalid file name: $rawName\"\n            }\n\n            val file = File(files.appDownloadsDir(), safeName)\n\n            if (file.exists()) {\n                Logger.d { \"Deleting existing file before download: ${file.absolutePath}\" }\n                file.delete()\n            }\n\n            Logger.d { \"saveToFile downloading file...\" }\n            download(url, suggestedFileName).collect { }\n\n            file.absolutePath\n        }\n\n    override suspend fun getDownloadedFilePath(fileName: String): String? =\n        withContext(Dispatchers.IO) {\n            val file = File(files.appDownloadsDir(), fileName)\n\n            if (file.exists() && file.length() > 0) {\n                file.absolutePath\n            } else {\n                null\n            }\n        }\n\n    override suspend fun cancelDownload(fileName: String): Boolean =\n        withContext(Dispatchers.IO) {\n            var cancelled = false\n            var deleted = false\n\n            val downloadId = activeFileNames[fileName]\n            if (downloadId != null) {\n                activeDownloads[downloadId]?.let { call: Call ->\n                    if (!call.isCanceled()) {\n                        call.cancel()\n                        cancelled = true\n                    }\n                    activeDownloads.remove(downloadId)\n                }\n                activeFileNames.remove(fileName)\n            }\n\n            val file = File(files.appDownloadsDir(), fileName)\n            if (file.exists()) {\n                deleted = file.delete()\n            }\n\n            cancelled || deleted\n        }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.Context\nimport java.io.File\n\nclass AndroidFileLocationsProvider(\n    private val context: Context,\n) : zed.rainxch.core.data.services.FileLocationsProvider {\n    override fun appDownloadsDir(): String {\n        val externalFilesRoot = context.getExternalFilesDir(null)\n        val dir = File(externalFilesRoot, \"ghs_downloads\")\n        if (!dir.exists()) dir.mkdirs()\n        return dir.absolutePath\n    }\n\n    override fun userDownloadsDir(): String {\n        return \"\" // No-op\n    }\n\n    override fun setExecutableIfNeeded(path: String) {\n        // No-op on Android\n    }\n\n    override fun getCacheSizeBytes(): Long {\n        val dir = File(appDownloadsDir())\n        return calculateDirSize(dir)\n    }\n\n    override fun clearCacheFiles(): Boolean {\n        val dir = File(appDownloadsDir())\n        return deleteDirectoryContents(dir)\n    }\n\n    private fun calculateDirSize(dir: File): Long {\n        if (!dir.exists()) return 0L\n        var size = 0L\n        dir.listFiles()?.forEach { file ->\n            size += if (file.isDirectory) calculateDirSize(file) else file.length()\n        }\n        return size\n    }\n\n    private fun deleteDirectoryContents(dir: File): Boolean {\n        if (!dir.exists()) return true\n        var allDeleted = true\n        dir.listFiles()?.forEach { file ->\n            if (file.isDirectory) {\n                if (!deleteDirectoryContents(file)) allDeleted = false\n                if (!file.delete()) allDeleted = false\n            } else {\n                if (!file.delete()) allDeleted = false\n            }\n        }\n        return allDeleted\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.provider.Settings\nimport androidx.core.content.FileProvider\nimport androidx.core.net.toUri\nimport co.touchlab.kermit.Logger\nimport zed.rainxch.core.domain.model.AssetArchitectureMatcher\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.SystemArchitecture\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.system.InstallerInfoExtractor\nimport java.io.File\n\nclass AndroidInstaller(\n    private val context: Context,\n    private val installerInfoExtractor: InstallerInfoExtractor,\n) : Installer {\n    override fun getApkInfoExtractor(): InstallerInfoExtractor = installerInfoExtractor\n\n    override fun detectSystemArchitecture(): SystemArchitecture {\n        val arch = Build.SUPPORTED_ABIS.firstOrNull() ?: return SystemArchitecture.UNKNOWN\n        return when {\n            arch.contains(\"arm64\") || arch.contains(\"aarch64\") -> SystemArchitecture.AARCH64\n            arch.contains(\"armeabi\") -> SystemArchitecture.ARM\n            arch.contains(\"x86_64\") -> SystemArchitecture.X86_64\n            arch.contains(\"x86\") -> SystemArchitecture.X86\n            else -> SystemArchitecture.UNKNOWN\n        }\n    }\n\n    override fun isAssetInstallable(assetName: String): Boolean {\n        val name = assetName.lowercase()\n        if (!name.endsWith(\".apk\")) return false\n        val systemArch = detectSystemArchitecture()\n        return isArchitectureCompatible(name, systemArch)\n    }\n\n    private fun isArchitectureCompatible(\n        assetName: String,\n        systemArch: SystemArchitecture,\n    ): Boolean = AssetArchitectureMatcher.isCompatible(assetName, systemArch)\n\n    override fun choosePrimaryAsset(assets: List<GithubAsset>): GithubAsset? {\n        if (assets.isEmpty()) return null\n        val systemArch = detectSystemArchitecture()\n        val compatibleAssets =\n            assets.filter { asset ->\n                isArchitectureCompatible(asset.name.lowercase(), systemArch)\n            }\n        val assetsToConsider = compatibleAssets.ifEmpty { assets }\n        return assetsToConsider.maxByOrNull { asset ->\n            val name = asset.name.lowercase()\n            val archBoost =\n                when (systemArch) {\n                    SystemArchitecture.X86_64 -> {\n                        if (AssetArchitectureMatcher.isExactMatch(\n                                name,\n                                SystemArchitecture.X86_64,\n                            )\n                        ) {\n                            10000\n                        } else {\n                            0\n                        }\n                    }\n\n                    SystemArchitecture.AARCH64 -> {\n                        if (AssetArchitectureMatcher.isExactMatch(\n                                name,\n                                SystemArchitecture.AARCH64,\n                            )\n                        ) {\n                            10000\n                        } else {\n                            0\n                        }\n                    }\n\n                    SystemArchitecture.X86 -> {\n                        if (AssetArchitectureMatcher.isExactMatch(\n                                name,\n                                SystemArchitecture.X86,\n                            )\n                        ) {\n                            10000\n                        } else {\n                            0\n                        }\n                    }\n\n                    SystemArchitecture.ARM -> {\n                        if (AssetArchitectureMatcher.isExactMatch(\n                                name,\n                                SystemArchitecture.ARM,\n                            )\n                        ) {\n                            10000\n                        } else {\n                            0\n                        }\n                    }\n\n                    SystemArchitecture.UNKNOWN -> {\n                        0\n                    }\n                }\n            archBoost + asset.size\n        }\n    }\n\n    override suspend fun isSupported(extOrMime: String): Boolean {\n        val ext = extOrMime.lowercase().removePrefix(\".\")\n        return ext == \"apk\"\n    }\n\n    override suspend fun ensurePermissionsOrThrow(extOrMime: String) {\n        val pm = context.packageManager\n        if (!pm.canRequestPackageInstalls()) {\n            val intent =\n                Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {\n                    data = \"package:${context.packageName}\".toUri()\n                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                }\n            context.startActivity(intent)\n            throw IllegalStateException(\"Please enable 'Install unknown apps' for this app in Settings and try again.\")\n        }\n    }\n\n    override suspend fun install(\n        filePath: String,\n        extOrMime: String,\n    ) {\n        val file = File(filePath)\n        if (!file.exists()) {\n            throw IllegalStateException(\"APK file not found: $filePath\")\n        }\n\n        Logger.d { \"Installing APK: $filePath\" }\n\n        val authority = \"${context.packageName}.fileprovider\"\n        val fileUri: Uri = FileProvider.getUriForFile(context, authority, file)\n\n        val intent =\n            Intent(Intent.ACTION_VIEW).apply {\n                setDataAndType(fileUri, \"application/vnd.android.package-archive\")\n                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n\n        if (intent.resolveActivity(context.packageManager) != null) {\n            context.startActivity(intent)\n            Logger.d { \"APK installation intent launched\" }\n        } else {\n            throw IllegalStateException(\"No installer available on this device\")\n        }\n    }\n\n    override fun uninstall(packageName: String) {\n        Logger.d { \"Requesting uninstall for: $packageName\" }\n        val intent =\n            Intent(Intent.ACTION_DELETE).apply {\n                data = \"package:$packageName\".toUri()\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n        try {\n            context.startActivity(intent)\n        } catch (e: Exception) {\n            Logger.w { \"Failed to start uninstall for $packageName: ${e.message}\" }\n        }\n    }\n\n    override fun isObtainiumInstalled(): Boolean =\n        try {\n            context.packageManager.getPackageInfo(\"dev.imranr.obtainium.fdroid\", 0)\n            true\n        } catch (e: Exception) {\n            try {\n                context.packageManager.getPackageInfo(\"dev.imranr.obtainium\", 0)\n                true\n            } catch (e: Exception) {\n                false\n            }\n        }\n\n    override fun openInObtainium(\n        repoOwner: String,\n        repoName: String,\n        onOpenInstaller: () -> Unit,\n    ) {\n        val obtainiumUrl = \"obtainium://add/https://github.com/$repoOwner/$repoName\"\n        val intent =\n            Intent(Intent.ACTION_VIEW).apply {\n                data = obtainiumUrl.toUri()\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n        try {\n            context.startActivity(intent)\n        } catch (_: ActivityNotFoundException) {\n            onOpenInstaller()\n        }\n    }\n\n    override fun isAppManagerInstalled(): Boolean =\n        try {\n            context.packageManager.getPackageInfo(\"io.github.muntashirakon.AppManager\", 0)\n            true\n        } catch (e: Exception) {\n            false\n        }\n\n    override fun openApp(packageName: String): Boolean {\n        val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)\n        return if (launchIntent != null) {\n            try {\n                launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                context.startActivity(launchIntent)\n                true\n            } catch (e: ActivityNotFoundException) {\n                Logger.w { \"Failed to launch $packageName: ${e.message}\" }\n                false\n            }\n        } else {\n            Logger.w { \"No launch intent found for $packageName\" }\n            false\n        }\n    }\n\n    override fun openWithExternalInstaller(filePath: String) {\n        val file = File(filePath)\n        if (!file.exists()) {\n            throw IllegalStateException(\"APK file not found: $filePath\")\n        }\n\n        Logger.d { \"Opening APK with external installer: $filePath\" }\n\n        val authority = \"${context.packageName}.fileprovider\"\n        val fileUri: Uri = FileProvider.getUriForFile(context, authority, file)\n\n        val intent =\n            Intent(Intent.ACTION_VIEW).apply {\n                setDataAndType(fileUri, \"application/vnd.android.package-archive\")\n                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n\n        val chooser =\n            Intent.createChooser(intent, null).apply {\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n        context.startActivity(chooser)\n    }\n\n    override fun openInAppManager(\n        filePath: String,\n        onOpenInstaller: () -> Unit,\n    ) {\n        val file = File(filePath)\n        if (!file.exists()) {\n            throw IllegalStateException(\"APK file not found: $filePath\")\n        }\n\n        Logger.d { \"Opening APK in AppManager: $filePath\" }\n\n        val authority = \"${context.packageName}.fileprovider\"\n        val fileUri: Uri = FileProvider.getUriForFile(context, authority, file)\n\n        val intent =\n            Intent(Intent.ACTION_VIEW).apply {\n                setDataAndType(fileUri, \"application/vnd.android.package-archive\")\n                setPackage(\"io.github.muntashirakon.AppManager\")\n                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n\n        try {\n            context.startActivity(intent)\n            Logger.d { \"APK opened in AppManager\" }\n        } catch (_: ActivityNotFoundException) {\n            onOpenInstaller()\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstallerInfoExtractor.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES\nimport android.os.Build\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.domain.model.ApkPackageInfo\nimport zed.rainxch.core.domain.system.InstallerInfoExtractor\nimport java.io.File\nimport java.security.MessageDigest\n\nclass AndroidInstallerInfoExtractor(\n    private val context: Context,\n) : InstallerInfoExtractor {\n    override suspend fun extractPackageInfo(filePath: String): ApkPackageInfo? =\n        withContext(Dispatchers.IO) {\n            try {\n                val packageManager = context.packageManager\n                val flags =\n                    PackageManager.GET_META_DATA or\n                        PackageManager.GET_ACTIVITIES or\n                        GET_SIGNING_CERTIFICATES\n\n                val packageInfo =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                        packageManager.getPackageArchiveInfo(\n                            filePath,\n                            PackageManager.PackageInfoFlags.of(flags.toLong()),\n                        )\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        packageManager.getPackageArchiveInfo(filePath, flags)\n                    }\n\n                if (packageInfo == null) {\n                    Logger.e {\n                        \"Failed to parse APK at $filePath, file exists: ${\n                            File(\n                                filePath,\n                            ).exists()\n                        }, size: ${File(filePath).length()}\"\n                    }\n                    return@withContext null\n                }\n\n                val appInfo = packageInfo.applicationInfo\n                appInfo?.sourceDir = filePath\n                appInfo?.publicSourceDir = filePath\n\n                val appName = appInfo?.let { packageManager.getApplicationLabel(it) }.toString()\n                val versionCode =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                        packageInfo.longVersionCode\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        packageInfo.versionCode.toLong()\n                    }\n                val fingerprint: String? =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                        val sigInfo = packageInfo.signingInfo\n                        val certs =\n                            if (sigInfo?.hasMultipleSigners() == true) {\n                                sigInfo.apkContentsSigners\n                            } else {\n                                sigInfo?.signingCertificateHistory\n                            }\n                        certs?.firstOrNull()?.toByteArray()?.let { certBytes ->\n                            MessageDigest\n                                .getInstance(\"SHA-256\")\n                                .digest(certBytes)\n                                .joinToString(\":\") { \"%02X\".format(it) }\n                        }\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        val legacyInfo =\n                            packageManager.getPackageArchiveInfo(\n                                filePath,\n                                PackageManager.GET_SIGNATURES,\n                            )\n                        @Suppress(\"DEPRECATION\")\n                        legacyInfo?.signatures?.firstOrNull()?.toByteArray()?.let { certBytes ->\n                            MessageDigest\n                                .getInstance(\"SHA-256\")\n                                .digest(certBytes)\n                                .joinToString(\":\") { \"%02X\".format(it) }\n                        }\n                    }\n\n                ApkPackageInfo(\n                    appName = appName,\n                    packageName = packageInfo.packageName,\n                    versionName = packageInfo.versionName ?: \"unknown\",\n                    versionCode = versionCode,\n                    signingFingerprint = fingerprint,\n                )\n            } catch (e: Exception) {\n                Logger.e { \"Failed to extract APK info: ${e.message}, file: $filePath\" }\n                null\n            }\n        }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidLocalizationManager.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport java.util.Locale\n\nclass AndroidLocalizationManager : zed.rainxch.core.data.services.LocalizationManager {\n    override fun getCurrentLanguageCode(): String {\n        val locale = Locale.getDefault()\n        val language = locale.language\n        val country = locale.country\n        return if (country.isNotEmpty()) {\n            \"$language-$country\"\n        } else {\n            language\n        }\n    }\n\n    override fun getPrimaryLanguageCode(): String = Locale.getDefault().language\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidPackageMonitor.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.Context\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageManager\nimport android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES\nimport android.os.Build\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.domain.model.DeviceApp\nimport zed.rainxch.core.domain.model.SystemPackageInfo\nimport zed.rainxch.core.domain.system.PackageMonitor\nimport java.security.MessageDigest\n\nclass AndroidPackageMonitor(\n    context: Context,\n) : PackageMonitor {\n    private val packageManager: PackageManager = context.packageManager\n\n    override suspend fun isPackageInstalled(packageName: String): Boolean = getInstalledPackageInfo(packageName) != null\n\n    override suspend fun getInstalledPackageInfo(packageName: String): SystemPackageInfo? =\n        withContext(Dispatchers.IO) {\n            runCatching {\n                val flags =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                        GET_SIGNING_CERTIFICATES.toLong()\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        PackageManager.GET_SIGNATURES.toLong()\n                    }\n\n                val packageInfo =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                        packageManager.getPackageInfo(\n                            packageName,\n                            PackageManager.PackageInfoFlags.of(flags),\n                        )\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        packageManager.getPackageInfo(packageName, flags.toInt())\n                    }\n\n                val versionCode =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                        packageInfo.longVersionCode\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        packageInfo.versionCode.toLong()\n                    }\n\n                val signingFingerprint: String? =\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                        val sigInfo = packageInfo.signingInfo\n                        val certs =\n                            if (sigInfo?.hasMultipleSigners() == true) {\n                                sigInfo.apkContentsSigners\n                            } else {\n                                sigInfo?.signingCertificateHistory\n                            }\n                        certs?.firstOrNull()?.toByteArray()?.let { certBytes ->\n                            MessageDigest\n                                .getInstance(\"SHA-256\")\n                                .digest(certBytes)\n                                .joinToString(\":\") { \"%02X\".format(it) }\n                        }\n                    } else {\n                        @Suppress(\"DEPRECATION\")\n                        packageInfo.signatures?.firstOrNull()?.toByteArray()?.let { certBytes ->\n                            MessageDigest\n                                .getInstance(\"SHA-256\")\n                                .digest(certBytes)\n                                .joinToString(\":\") { \"%02X\".format(it) }\n                        }\n                    }\n\n                SystemPackageInfo(\n                    packageName = packageInfo.packageName,\n                    versionName = packageInfo.versionName ?: \"unknown\",\n                    versionCode = versionCode,\n                    isInstalled = true,\n                    signingFingerprint = signingFingerprint,\n                )\n            }.getOrNull()\n        }\n\n    override suspend fun getAllInstalledPackageNames(): Set<String> =\n        withContext(Dispatchers.IO) {\n            val packages =\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(0L))\n                } else {\n                    @Suppress(\"DEPRECATION\")\n                    packageManager.getInstalledPackages(0)\n                }\n\n            packages.map { it.packageName }.toSet()\n        }\n\n    override suspend fun getAllInstalledApps(): List<DeviceApp> =\n        withContext(Dispatchers.IO) {\n            val packages =\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(0L))\n                } else {\n                    @Suppress(\"DEPRECATION\")\n                    packageManager.getInstalledPackages(0)\n                }\n\n            packages\n                .filter { pkg ->\n                    val isSystemApp = (pkg.applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM != 0\n                    val isUpdatedSystem = (pkg.applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0\n                    !isSystemApp || isUpdatedSystem\n                }.map { pkg ->\n                    val versionCode =\n                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                            pkg.longVersionCode\n                        } else {\n                            @Suppress(\"DEPRECATION\")\n                            pkg.versionCode.toLong()\n                        }\n\n                    DeviceApp(\n                        packageName = pkg.packageName,\n                        appName = pkg.applicationInfo?.loadLabel(packageManager)?.toString() ?: pkg.packageName,\n                        versionName = pkg.versionName,\n                        versionCode = versionCode,\n                        signingFingerprint = null,\n                    )\n                }.sortedBy { it.appName.lowercase() }\n        }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidUpdateScheduleManager.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.Context\nimport zed.rainxch.core.domain.system.UpdateScheduleManager\n\nclass AndroidUpdateScheduleManager(\n    private val context: Context,\n) : UpdateScheduleManager {\n    override fun reschedule(intervalHours: Long) {\n        UpdateScheduler.reschedule(context, intervalHours)\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AutoUpdateWorker.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.content.pm.ServiceInfo\nimport android.os.Build\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.content.ContextCompat\nimport androidx.work.CoroutineWorker\nimport androidx.work.ForegroundInfo\nimport androidx.work.WorkerParameters\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.flow.first\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport zed.rainxch.core.data.services.shizuku.ShizukuServiceManager\nimport zed.rainxch.core.data.services.shizuku.model.ShizukuStatus\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.core.domain.network.Downloader\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.Installer\n\n/**\n * Background worker that automatically downloads and silently installs\n * available updates via Shizuku.\n *\n * Only runs when auto-update is enabled AND Shizuku installer is selected and READY.\n * Falls back gracefully: if Shizuku becomes unavailable mid-update, remaining apps\n * are skipped and a notification is shown for manual update.\n */\nclass AutoUpdateWorker(\n    context: Context,\n    params: WorkerParameters,\n) : CoroutineWorker(context, params),\n    KoinComponent {\n    private val installedAppsRepository: InstalledAppsRepository by inject()\n    private val installer: Installer by inject()\n    private val downloader: Downloader by inject()\n    private val tweaksRepository: TweaksRepository by inject()\n    private val shizukuServiceManager: ShizukuServiceManager by inject()\n\n    override suspend fun doWork(): Result {\n        return try {\n            Logger.i { \"AutoUpdateWorker: Starting auto-update\" }\n\n            val autoUpdateEnabled = tweaksRepository.getAutoUpdateEnabled().first()\n            val installerType = tweaksRepository.getInstallerType().first()\n\n            shizukuServiceManager.refreshStatus()\n            val shizukuReady = shizukuServiceManager.status.value == ShizukuStatus.READY\n\n            if (!autoUpdateEnabled || installerType != InstallerType.SHIZUKU || !shizukuReady) {\n                Logger.i {\n                    \"AutoUpdateWorker: Conditions not met (autoUpdate=$autoUpdateEnabled, installer=$installerType, shizuku=$shizukuReady), skipping\"\n                }\n                return Result.success()\n            }\n\n            val appsWithUpdates = installedAppsRepository.getAppsWithUpdates().first()\n            if (appsWithUpdates.isEmpty()) {\n                Logger.d { \"AutoUpdateWorker: No apps need updating\" }\n                return Result.success()\n            }\n\n            setForeground(createForegroundInfo(\"Updating apps...\", 0, appsWithUpdates.size))\n\n            val successfulApps = mutableListOf<String>()\n            val failedApps = mutableListOf<String>()\n\n            appsWithUpdates.forEachIndexed { index, app ->\n                setForeground(\n                    createForegroundInfo(\n                        \"Updating ${app.appName}...\",\n                        index + 1,\n                        appsWithUpdates.size,\n                    ),\n                )\n\n                try {\n                    updateApp(app)\n                    successfulApps.add(app.appName)\n                    Logger.i { \"AutoUpdateWorker: Successfully updated ${app.appName}\" }\n                } catch (e: Exception) {\n                    failedApps.add(app.appName)\n                    Logger.e { \"AutoUpdateWorker: Failed to update ${app.appName}: ${e.message}\" }\n                    try {\n                        installedAppsRepository.updatePendingStatus(app.packageName, false)\n                    } catch (clearEx: Exception) {\n                        Logger.e { \"AutoUpdateWorker: Failed to clear pending status: ${clearEx.message}\" }\n                    }\n                }\n            }\n\n            showSummaryNotification(successfulApps, failedApps)\n\n            Logger.i { \"AutoUpdateWorker: Completed. Success: ${successfulApps.size}, Failed: ${failedApps.size}\" }\n            Result.success()\n        } catch (e: Exception) {\n            Logger.e { \"AutoUpdateWorker: Fatal error: ${e.message}\" }\n            if (runAttemptCount < 2) {\n                Result.retry()\n            } else {\n                Result.failure()\n            }\n        }\n    }\n\n    private suspend fun updateApp(app: InstalledApp) {\n        val assetUrl =\n            app.latestAssetUrl\n                ?: throw IllegalStateException(\"No asset URL for ${app.appName}\")\n        val assetName =\n            app.latestAssetName\n                ?: throw IllegalStateException(\"No asset name for ${app.appName}\")\n        val latestVersion =\n            app.latestVersion\n                ?: throw IllegalStateException(\"No latest version for ${app.appName}\")\n\n        val ext = assetName.substringAfterLast('.', \"\").lowercase()\n\n        val existingPath = downloader.getDownloadedFilePath(assetName)\n        if (existingPath != null) {\n            val file = java.io.File(existingPath)\n            try {\n                val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(existingPath)\n                val normalizedExisting =\n                    apkInfo?.versionName?.removePrefix(\"v\")?.removePrefix(\"V\") ?: \"\"\n                val normalizedLatest = latestVersion.removePrefix(\"v\").removePrefix(\"V\")\n                if (normalizedExisting != normalizedLatest) {\n                    file.delete()\n                    Logger.d { \"AutoUpdateWorker: Deleted mismatched existing file for ${app.appName}\" }\n                }\n            } catch (_: Exception) {\n                file.delete()\n                Logger.d { \"AutoUpdateWorker: Deleted unextractable existing file for ${app.appName}\" }\n            }\n        }\n\n        Logger.d { \"AutoUpdateWorker: Downloading $assetName for ${app.appName}\" }\n        downloader.download(assetUrl, assetName).collect { /* consume flow to completion */ }\n\n        val filePath =\n            downloader.getDownloadedFilePath(assetName)\n                ?: throw IllegalStateException(\"Downloaded file not found for ${app.appName}\")\n\n        val apkInfo =\n            installer.getApkInfoExtractor().extractPackageInfo(filePath)\n                ?: throw IllegalStateException(\"Failed to extract APK info for ${app.appName}\")\n\n        // Validate package name matches\n        if (apkInfo.packageName != app.packageName) {\n            Logger.e {\n                \"AutoUpdateWorker: Package name mismatch for ${app.appName}! \" +\n                    \"Expected: ${app.packageName}, got: ${apkInfo.packageName}. \" +\n                    \"Skipping auto-update.\"\n            }\n            throw IllegalStateException(\n                \"Package name mismatch for ${app.appName}: expected ${app.packageName}, got ${apkInfo.packageName}\",\n            )\n        }\n\n        val currentApp = installedAppsRepository.getAppByPackage(app.packageName)\n\n        if (currentApp?.signingFingerprint != null) {\n            val expected = currentApp.signingFingerprint!!.trim().uppercase()\n            val actual = apkInfo.signingFingerprint?.trim()?.uppercase()\n            if (actual == null || expected != actual) {\n                Logger.e {\n                    \"AutoUpdateWorker: Signing key mismatch for ${app.appName}! \" +\n                        \"Expected: ${currentApp.signingFingerprint}, got: ${apkInfo.signingFingerprint}. \" +\n                        \"Skipping auto-update.\"\n                }\n                throw IllegalStateException(\n                    \"Signing fingerprint verification failed for ${app.appName}, blocking auto-update\",\n                )\n            }\n\n            installedAppsRepository.updateApp(\n                currentApp.copy(\n                    isPendingInstall = true,\n                    latestVersion = latestVersion,\n                    latestAssetName = assetName,\n                    latestAssetUrl = assetUrl,\n                    latestVersionName = apkInfo.versionName,\n                    latestVersionCode = apkInfo.versionCode,\n                ),\n            )\n\n            Logger.d { \"AutoUpdateWorker: Installing ${app.appName} via Shizuku\" }\n            try {\n                installer.install(filePath, ext)\n            } catch (e: Exception) {\n                installedAppsRepository.updatePendingStatus(app.packageName, false)\n                throw e\n            }\n\n            Logger.d { \"AutoUpdateWorker: Install command completed for ${app.appName}, waiting for system confirmation via broadcast\" }\n        }\n    }\n\n    private fun createForegroundInfo(\n        message: String,\n        current: Int,\n        total: Int,\n    ): ForegroundInfo {\n        val builder =\n            NotificationCompat\n                .Builder(applicationContext, UPDATE_SERVICE_CHANNEL_ID)\n                .setSmallIcon(android.R.drawable.stat_sys_download)\n                .setContentTitle(\"GitHub Store\")\n                .setContentText(message)\n                .setPriority(NotificationCompat.PRIORITY_LOW)\n                .setOngoing(true)\n                .setSilent(true)\n\n        if (total > 0) {\n            builder.setProgress(total, current, false)\n        }\n\n        val notification = builder.build()\n\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            ForegroundInfo(\n                FOREGROUND_NOTIFICATION_ID,\n                notification,\n                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,\n            )\n        } else {\n            ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification)\n        }\n    }\n\n    @SuppressLint(\"MissingPermission\")\n    private fun showSummaryNotification(\n        successfulApps: List<String>,\n        failedApps: List<String>,\n    ) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            val granted =\n                ContextCompat.checkSelfPermission(\n                    applicationContext,\n                    Manifest.permission.POST_NOTIFICATIONS,\n                ) == PackageManager.PERMISSION_GRANTED\n            if (!granted) return\n        }\n\n        if (successfulApps.isEmpty() && failedApps.isEmpty()) return\n\n        val title =\n            when {\n                failedApps.isEmpty() -> \"${successfulApps.size} app${if (successfulApps.size > 1) \"s\" else \"\"} updated\"\n                successfulApps.isEmpty() -> \"Failed to update ${failedApps.size} app${if (failedApps.size > 1) \"s\" else \"\"}\"\n                else -> \"${successfulApps.size} updated, ${failedApps.size} failed\"\n            }\n\n        val text =\n            when {\n                failedApps.isEmpty() -> successfulApps.joinToString(\", \")\n\n                successfulApps.isEmpty() -> failedApps.joinToString(\", \")\n\n                else -> \"Updated: ${successfulApps.joinToString(\", \")}. Failed: ${\n                    failedApps.joinToString(\n                        \", \",\n                    )\n                }\"\n            }\n\n        val launchIntent =\n            applicationContext.packageManager\n                .getLaunchIntentForPackage(applicationContext.packageName)\n                ?.apply {\n                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP\n                }\n\n        val pendingIntent =\n            launchIntent?.let {\n                PendingIntent.getActivity(\n                    applicationContext,\n                    0,\n                    it,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n            }\n\n        val notification =\n            NotificationCompat\n                .Builder(applicationContext, UPDATES_CHANNEL_ID)\n                .setSmallIcon(\n                    if (failedApps.isEmpty()) {\n                        android.R.drawable.stat_sys_download_done\n                    } else {\n                        android.R.drawable.stat_notify_error\n                    },\n                ).setContentTitle(title)\n                .setContentText(text)\n                .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n                .setContentIntent(pendingIntent)\n                .setAutoCancel(true)\n                .build()\n\n        NotificationManagerCompat\n            .from(applicationContext)\n            .notify(SUMMARY_NOTIFICATION_ID, notification)\n    }\n\n    companion object {\n        const val WORK_NAME = \"github_store_auto_update\"\n        private const val UPDATES_CHANNEL_ID = \"app_updates\"\n        private const val UPDATE_SERVICE_CHANNEL_ID = \"update_service\"\n        private const val FOREGROUND_NOTIFICATION_ID = 1004\n        private const val SUMMARY_NOTIFICATION_ID = 1005\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/BootReceiver.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport co.touchlab.kermit.Logger\n\n/**\n * Reschedules periodic update checks after device reboot.\n * Registered statically in AndroidManifest.xml.\n */\nclass BootReceiver : BroadcastReceiver() {\n    override fun onReceive(context: Context, intent: Intent?) {\n        if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {\n            Logger.i { \"BootReceiver: Device booted, scheduling update checks\" }\n            UpdateScheduler.schedule(context)\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.system.PackageMonitor\n\n/**\n * Listens for package install/replace/remove broadcasts to update tracked app state.\n *\n * Registered both statically (manifest — works when process is dead, e.g. after\n * Shizuku silent install) and dynamically (GithubStoreApp — immediate in-process delivery).\n *\n * Uses [KoinComponent] for the no-arg constructor path (manifest-registered).\n * The constructor with explicit dependencies is used for dynamic registration.\n */\nclass PackageEventReceiver() :\n    BroadcastReceiver(),\n    KoinComponent {\n    private val installedAppsRepositoryKoin: InstalledAppsRepository by inject()\n    private val packageMonitorKoin: PackageMonitor by inject()\n\n    // Explicitly provided dependencies (dynamic registration path)\n    private var explicitRepository: InstalledAppsRepository? = null\n    private var explicitMonitor: PackageMonitor? = null\n\n    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)\n\n    constructor(\n        installedAppsRepository: InstalledAppsRepository,\n        packageMonitor: PackageMonitor,\n    ) : this() {\n        this.explicitRepository = installedAppsRepository\n        this.explicitMonitor = packageMonitor\n    }\n\n    private fun getRepository(): InstalledAppsRepository = explicitRepository ?: installedAppsRepositoryKoin\n\n    private fun getMonitor(): PackageMonitor = explicitMonitor ?: packageMonitorKoin\n\n    override fun onReceive(\n        context: Context?,\n        intent: Intent?,\n    ) {\n        val packageName = intent?.data?.schemeSpecificPart ?: return\n\n        Logger.d { \"PackageEventReceiver: ${intent.action} for $packageName\" }\n\n        try {\n            when (intent.action) {\n                Intent.ACTION_PACKAGE_ADDED,\n                Intent.ACTION_PACKAGE_REPLACED,\n                -> {\n                    scope.launch { onPackageInstalled(packageName) }\n                }\n\n                Intent.ACTION_PACKAGE_FULLY_REMOVED -> {\n                    scope.launch { onPackageRemoved(packageName) }\n                }\n            }\n        } catch (e: Exception) {\n            Logger.e { \"PackageEventReceiver: Failed to handle ${intent.action}: ${e.message}\" }\n        }\n    }\n\n    private suspend fun onPackageInstalled(packageName: String) {\n        try {\n            val repo = getRepository()\n            val monitor = getMonitor()\n            val app = repo.getAppByPackage(packageName) ?: return\n\n            if (app.isPendingInstall) {\n                val systemInfo = monitor.getInstalledPackageInfo(packageName)\n                if (systemInfo != null) {\n                    val expectedVersionCode = app.latestVersionCode ?: 0L\n                    val wasActuallyUpdated =\n                        expectedVersionCode > 0L &&\n                            systemInfo.versionCode >= expectedVersionCode\n\n                    if (wasActuallyUpdated) {\n                        repo.updateAppVersion(\n                            packageName = packageName,\n                            newTag = app.latestVersion ?: systemInfo.versionName,\n                            newAssetName = app.latestAssetName ?: \"\",\n                            newAssetUrl = app.latestAssetUrl ?: \"\",\n                            newVersionName = systemInfo.versionName,\n                            newVersionCode = systemInfo.versionCode,\n                            signingFingerprint = app.signingFingerprint,\n                        )\n                        repo.updatePendingStatus(packageName, false)\n                        Logger.i { \"Update confirmed via broadcast: $packageName (v${systemInfo.versionName})\" }\n                    } else {\n                        repo.updateApp(\n                            app.copy(\n                                isPendingInstall = false,\n                                installedVersionName = systemInfo.versionName,\n                                installedVersionCode = systemInfo.versionCode,\n                                isUpdateAvailable =\n                                    (\n                                        app.latestVersionCode\n                                            ?: 0L\n                                    ) > systemInfo.versionCode,\n                            ),\n                        )\n                        Logger.i {\n                            \"Package replaced but not updated to target: $packageName \" +\n                                \"(system: v${systemInfo.versionName}/${systemInfo.versionCode}, \" +\n                                \"target: v${app.latestVersionName}/${app.latestVersionCode})\"\n                        }\n                    }\n                } else {\n                    repo.updatePendingStatus(packageName, false)\n                    Logger.i { \"Resolved pending install via broadcast (no system info): $packageName\" }\n                }\n            } else {\n                val systemInfo = monitor.getInstalledPackageInfo(packageName)\n                if (systemInfo != null) {\n                    repo.updateApp(\n                        app.copy(\n                            installedVersionName = systemInfo.versionName,\n                            installedVersionCode = systemInfo.versionCode,\n                        ),\n                    )\n                    Logger.d { \"Updated version info via broadcast: $packageName (v${systemInfo.versionName})\" }\n                }\n            }\n        } catch (e: Exception) {\n            Logger.e { \"PackageEventReceiver error for $packageName: ${e.message}\" }\n        }\n    }\n\n    private suspend fun onPackageRemoved(packageName: String) {\n        try {\n            getRepository().deleteInstalledApp(packageName)\n            Logger.i { \"Removed uninstalled app via broadcast: $packageName\" }\n        } catch (e: Exception) {\n            Logger.e { \"PackageEventReceiver remove error for $packageName: ${e.message}\" }\n        }\n    }\n\n    companion object {\n        fun createIntentFilter(): IntentFilter =\n            IntentFilter().apply {\n                addAction(Intent.ACTION_PACKAGE_ADDED)\n                addAction(Intent.ACTION_PACKAGE_REPLACED)\n                addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)\n                addDataScheme(\"package\")\n            }\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateCheckWorker.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.content.pm.ServiceInfo\nimport android.os.Build\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.content.ContextCompat\nimport androidx.work.CoroutineWorker\nimport androidx.work.ForegroundInfo\nimport androidx.work.WorkerParameters\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.flow.first\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\n\n/**\n * Periodic background worker that checks all tracked installed apps for available updates.\n *\n * Runs via WorkManager on a configurable schedule (default: every 6 hours).\n * First syncs app state with the system package manager, then checks each\n * tracked app's GitHub repository for new releases.\n * Shows a notification when updates are found, or triggers auto-update\n * if Shizuku silent install is enabled and auto-update preference is on.\n */\nclass UpdateCheckWorker(\n    context: Context,\n    params: WorkerParameters,\n) : CoroutineWorker(context, params),\n    KoinComponent {\n    private val installedAppsRepository: InstalledAppsRepository by inject()\n    private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase by inject()\n    private val tweaksRepository: TweaksRepository by inject()\n\n    override suspend fun doWork(): Result =\n        try {\n            Logger.i { \"UpdateCheckWorker: Starting periodic update check\" }\n\n            // Run as foreground service to prevent OS from killing the worker\n            setForeground(createForegroundInfo(\"Checking for updates...\"))\n\n            // First sync installed apps state with system\n            val syncResult = syncInstalledAppsUseCase()\n            if (syncResult.isFailure) {\n                Logger.w { \"UpdateCheckWorker: Sync had issues: ${syncResult.exceptionOrNull()?.message}\" }\n            }\n\n            // Check all tracked apps for updates\n            installedAppsRepository.checkAllForUpdates()\n\n            val appsWithUpdates = installedAppsRepository.getAppsWithUpdates().first()\n\n            if (appsWithUpdates.isNotEmpty()) {\n                // Check if auto-update via Shizuku is enabled\n                val autoUpdateEnabled = tweaksRepository.getAutoUpdateEnabled().first()\n                val installerType = tweaksRepository.getInstallerType().first()\n\n                if (autoUpdateEnabled && installerType == InstallerType.SHIZUKU) {\n                    Logger.i {\n                        \"UpdateCheckWorker: Auto-update enabled with Shizuku, scheduling AutoUpdateWorker for ${appsWithUpdates.size} apps\"\n                    }\n                    UpdateScheduler.scheduleAutoUpdate(applicationContext)\n                } else {\n                    // Show notification for manual update\n                    showUpdateNotification(appsWithUpdates)\n                }\n            } else {\n                Logger.d { \"UpdateCheckWorker: No updates available\" }\n            }\n\n            Logger.i { \"UpdateCheckWorker: Periodic update check completed successfully\" }\n            Result.success()\n        } catch (e: Exception) {\n            Logger.e { \"UpdateCheckWorker: Update check failed: ${e.message}\" }\n            if (runAttemptCount < 3) {\n                Result.retry()\n            } else {\n                Result.failure()\n            }\n        }\n\n    private fun createForegroundInfo(message: String): ForegroundInfo {\n        val notification =\n            NotificationCompat\n                .Builder(applicationContext, UPDATE_SERVICE_CHANNEL_ID)\n                .setSmallIcon(android.R.drawable.stat_notify_sync)\n                .setContentTitle(\"GitHub Store\")\n                .setContentText(message)\n                .setPriority(NotificationCompat.PRIORITY_LOW)\n                .setOngoing(true)\n                .setSilent(true)\n                .build()\n\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            ForegroundInfo(\n                FOREGROUND_NOTIFICATION_ID,\n                notification,\n                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,\n            )\n        } else {\n            ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification)\n        }\n    }\n\n    @SuppressLint(\"MissingPermission\") // Permission checked at runtime before notify()\n    private suspend fun showUpdateNotification(appsWithUpdates: List<zed.rainxch.core.domain.model.InstalledApp>) {\n        // Check notification permission for API 33+\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            val granted =\n                ContextCompat.checkSelfPermission(\n                    applicationContext,\n                    Manifest.permission.POST_NOTIFICATIONS,\n                ) == PackageManager.PERMISSION_GRANTED\n            if (!granted) {\n                Logger.w { \"UpdateCheckWorker: POST_NOTIFICATIONS permission not granted, skipping notification\" }\n                return\n            }\n        }\n\n        val title =\n            if (appsWithUpdates.size == 1) {\n                \"${appsWithUpdates.first().appName} update available\"\n            } else {\n                \"${appsWithUpdates.size} app updates available\"\n            }\n\n        val text =\n            if (appsWithUpdates.size == 1) {\n                val app = appsWithUpdates.first()\n                \"${app.installedVersion} → ${app.latestVersion}\"\n            } else {\n                appsWithUpdates.joinToString(\", \") { it.appName }\n            }\n\n        val launchIntent =\n            applicationContext.packageManager\n                .getLaunchIntentForPackage(applicationContext.packageName)\n                ?.apply {\n                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP\n                }\n\n        val pendingIntent =\n            launchIntent?.let {\n                PendingIntent.getActivity(\n                    applicationContext,\n                    0,\n                    it,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n            }\n\n        val notification =\n            NotificationCompat\n                .Builder(applicationContext, UPDATES_CHANNEL_ID)\n                .setSmallIcon(android.R.drawable.stat_sys_download_done)\n                .setContentTitle(title)\n                .setContentText(text)\n                .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n                .setContentIntent(pendingIntent)\n                .setAutoCancel(true)\n                .build()\n\n        NotificationManagerCompat.from(applicationContext).notify(NOTIFICATION_ID, notification)\n        Logger.i { \"UpdateCheckWorker: Showed notification for ${appsWithUpdates.size} updates\" }\n    }\n\n    companion object {\n        const val WORK_NAME = \"github_store_update_check\"\n        private const val UPDATES_CHANNEL_ID = \"app_updates\"\n        private const val UPDATE_SERVICE_CHANNEL_ID = \"update_service\"\n        private const val NOTIFICATION_ID = 1001\n        private const val FOREGROUND_NOTIFICATION_ID = 1003\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport android.content.Context\nimport androidx.work.BackoffPolicy\nimport androidx.work.Constraints\nimport androidx.work.ExistingPeriodicWorkPolicy\nimport androidx.work.ExistingWorkPolicy\nimport androidx.work.NetworkType\nimport androidx.work.OneTimeWorkRequestBuilder\nimport androidx.work.PeriodicWorkRequestBuilder\nimport androidx.work.WorkManager\nimport co.touchlab.kermit.Logger\nimport java.util.concurrent.TimeUnit\n\nobject UpdateScheduler {\n    private const val DEFAULT_INTERVAL_HOURS = 6L\n    private const val IMMEDIATE_CHECK_WORK_NAME = \"github_store_immediate_update_check\"\n\n    fun schedule(\n        context: Context,\n        intervalHours: Long = DEFAULT_INTERVAL_HOURS,\n    ) {\n        val constraints =\n            Constraints\n                .Builder()\n                .setRequiredNetworkType(NetworkType.CONNECTED)\n                .build()\n\n        val request =\n            PeriodicWorkRequestBuilder<UpdateCheckWorker>(\n                repeatInterval = intervalHours,\n                repeatIntervalTimeUnit = TimeUnit.HOURS,\n            ).setConstraints(constraints)\n                .setBackoffCriteria(\n                    BackoffPolicy.EXPONENTIAL,\n                    30,\n                    TimeUnit.MINUTES,\n                ).build()\n\n        WorkManager\n            .getInstance(context)\n            .enqueueUniquePeriodicWork(\n                uniqueWorkName = UpdateCheckWorker.WORK_NAME,\n                existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP,\n                request = request,\n            )\n\n        val immediateRequest =\n            OneTimeWorkRequestBuilder<UpdateCheckWorker>()\n                .setConstraints(constraints)\n                .setInitialDelay(1, TimeUnit.MINUTES)\n                .build()\n\n        WorkManager\n            .getInstance(context)\n            .enqueueUniqueWork(\n                IMMEDIATE_CHECK_WORK_NAME,\n                ExistingWorkPolicy.REPLACE,\n                immediateRequest,\n            )\n\n        Logger.i { \"UpdateScheduler: Scheduled periodic update check every ${intervalHours}h + immediate check\" }\n    }\n\n    fun reschedule(\n        context: Context,\n        intervalHours: Long,\n    ) {\n        val constraints =\n            Constraints\n                .Builder()\n                .setRequiredNetworkType(NetworkType.CONNECTED)\n                .build()\n\n        val request =\n            PeriodicWorkRequestBuilder<UpdateCheckWorker>(\n                repeatInterval = intervalHours,\n                repeatIntervalTimeUnit = TimeUnit.HOURS,\n            ).setConstraints(constraints)\n                .setBackoffCriteria(\n                    BackoffPolicy.EXPONENTIAL,\n                    30,\n                    TimeUnit.MINUTES,\n                ).build()\n\n        WorkManager\n            .getInstance(context)\n            .enqueueUniquePeriodicWork(\n                uniqueWorkName = UpdateCheckWorker.WORK_NAME,\n                existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE,\n                request = request,\n            )\n\n        Logger.i { \"UpdateScheduler: Rescheduled periodic update check to every ${intervalHours}h\" }\n    }\n\n    fun scheduleAutoUpdate(context: Context) {\n        val constraints =\n            Constraints\n                .Builder()\n                .setRequiredNetworkType(NetworkType.CONNECTED)\n                .build()\n\n        val request =\n            OneTimeWorkRequestBuilder<AutoUpdateWorker>()\n                .setConstraints(constraints)\n                .setBackoffCriteria(\n                    BackoffPolicy.EXPONENTIAL,\n                    15,\n                    TimeUnit.MINUTES,\n                ).build()\n\n        WorkManager\n            .getInstance(context)\n            .enqueueUniqueWork(\n                AutoUpdateWorker.WORK_NAME,\n                ExistingWorkPolicy.KEEP,\n                request,\n            )\n\n        Logger.i { \"UpdateScheduler: Scheduled auto-update worker\" }\n    }\n\n    fun cancel(context: Context) {\n        WorkManager\n            .getInstance(context)\n            .cancelUniqueWork(UpdateCheckWorker.WORK_NAME)\n        WorkManager\n            .getInstance(context)\n            .cancelUniqueWork(IMMEDIATE_CHECK_WORK_NAME)\n        WorkManager\n            .getInstance(context)\n            .cancelUniqueWork(AutoUpdateWorker.WORK_NAME)\n        Logger.i { \"UpdateScheduler: Cancelled all update work\" }\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/AndroidInstallerStatusProvider.kt",
    "content": "package zed.rainxch.core.data.services.shizuku\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport zed.rainxch.core.data.services.shizuku.model.ShizukuStatus\nimport zed.rainxch.core.domain.model.ShizukuAvailability\nimport zed.rainxch.core.domain.system.InstallerStatusProvider\n\n/**\n * Android implementation of [InstallerStatusProvider].\n * Maps [ShizukuServiceManager.status] to the platform-agnostic [ShizukuAvailability] enum.\n */\nclass AndroidInstallerStatusProvider(\n    private val shizukuServiceManager: ShizukuServiceManager,\n    scope: CoroutineScope,\n) : InstallerStatusProvider {\n    override val shizukuAvailability: StateFlow<ShizukuAvailability> =\n        shizukuServiceManager.status\n            .map { status ->\n                when (status) {\n                    ShizukuStatus.NOT_INSTALLED -> ShizukuAvailability.UNAVAILABLE\n                    ShizukuStatus.NOT_RUNNING -> ShizukuAvailability.NOT_RUNNING\n                    ShizukuStatus.PERMISSION_NEEDED -> ShizukuAvailability.PERMISSION_NEEDED\n                    ShizukuStatus.READY -> ShizukuAvailability.READY\n                }\n            }.stateIn(\n                scope = scope,\n                started = SharingStarted.Eagerly,\n                initialValue = ShizukuAvailability.UNAVAILABLE,\n            )\n\n    override fun requestShizukuPermission() {\n        shizukuServiceManager.requestPermission()\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerServiceImpl.kt",
    "content": "package zed.rainxch.core.data.services.shizuku\n\nimport android.os.ParcelFileDescriptor\nimport java.io.BufferedReader\nimport java.io.InputStreamReader\nimport java.util.concurrent.TimeUnit\n\n/**\n * Shizuku UserService implementation that runs in a privileged process (shell/root).\n * Provides silent package install/uninstall via `pm` shell commands.\n *\n * This class runs in Shizuku's process, NOT in the app's process.\n * It has shell-level (UID 2000) or root-level (UID 0) privileges.\n *\n * Uses `pm install` with stdin pipe for install, `pm uninstall` for uninstall.\n * This is the most reliable approach — avoids fragile reflection on hidden\n * IPackageInstaller/IPackageInstallerSession/IIntentSender APIs.\n *\n * MUST have a default no-arg constructor for Shizuku's UserService framework.\n */\nclass ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {\n\n    companion object {\n        private const val TAG = \"ShizukuService\"\n\n        private const val STATUS_SUCCESS = 0\n        private const val STATUS_FAILURE = -1\n        private const val INSTALL_TIMEOUT_SECONDS = 120L\n        private const val UNINSTALL_TIMEOUT_SECONDS = 30L\n\n        private fun log(msg: String) = android.util.Log.d(TAG, msg)\n        private fun logW(msg: String) = android.util.Log.w(TAG, msg)\n        private fun logE(msg: String, e: Throwable? = null) = android.util.Log.e(TAG, msg, e)\n    }\n\n    override fun installPackage(pfd: ParcelFileDescriptor, fileSize: Long): Int {\n        log(\"installPackage() called — fileSize=$fileSize\")\n        log(\"Process UID: ${android.os.Process.myUid()}, PID: ${android.os.Process.myPid()}\")\n\n        return try {\n            // Use \"pm install -S <size>\" which reads the APK from stdin\n            val command = arrayOf(\"pm\", \"install\", \"-S\", fileSize.toString())\n            log(\"Executing: ${command.joinToString(\" \")}\")\n\n            val process = Runtime.getRuntime().exec(command)\n\n            // Pipe the APK from the ParcelFileDescriptor to pm's stdin\n            val writeThread = Thread {\n                try {\n                    ParcelFileDescriptor.AutoCloseInputStream(pfd).use { input ->\n                        process.outputStream.use { output ->\n                            val buffer = ByteArray(65536)\n                            var bytesWritten = 0L\n                            var read: Int\n                            while (input.read(buffer).also { read = it } != -1) {\n                                output.write(buffer, 0, read)\n                                bytesWritten += read\n                            }\n                            output.flush()\n                            log(\"APK piped to pm stdin: $bytesWritten bytes (expected: $fileSize)\")\n                        }\n                    }\n                } catch (e: Exception) {\n                    logE(\"Error piping APK to pm stdin\", e)\n                }\n            }\n            writeThread.start()\n\n            // Read stdout/stderr for result\n            val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim()\n            val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim()\n\n            writeThread.join(INSTALL_TIMEOUT_SECONDS * 1000)\n            if (writeThread.isAlive) {\n                logE(\"Write thread timed out after ${INSTALL_TIMEOUT_SECONDS}s, aborting install\")\n                writeThread.interrupt()\n                process.destroyForcibly()\n                return STATUS_FAILURE\n            }\n\n            val finished = process.waitFor(INSTALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n            if (!finished) {\n                logE(\"pm install timed out after ${INSTALL_TIMEOUT_SECONDS}s, aborting\")\n                process.destroyForcibly()\n                return STATUS_FAILURE\n            }\n\n            val exitCode = process.exitValue()\n            log(\"pm install — exitCode=$exitCode, stdout='$stdout', stderr='$stderr'\")\n\n            if (exitCode == 0 && stdout.contains(\"Success\")) {\n                log(\"Install SUCCESS\")\n                STATUS_SUCCESS\n            } else {\n                logE(\"Install FAILED — exitCode=$exitCode, stdout='$stdout', stderr='$stderr'\")\n                STATUS_FAILURE\n            }\n        } catch (e: Exception) {\n            logE(\"installPackage() exception\", e)\n            STATUS_FAILURE\n        }\n    }\n\n    override fun uninstallPackage(packageName: String): Int {\n        log(\"uninstallPackage() called for: $packageName\")\n        return try {\n            val command = arrayOf(\"pm\", \"uninstall\", packageName)\n            log(\"Executing: ${command.joinToString(\" \")}\")\n\n            val process = Runtime.getRuntime().exec(command)\n            val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim()\n            val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim()\n\n            val finished = process.waitFor(UNINSTALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n            if (!finished) {\n                logE(\"pm uninstall timed out after ${UNINSTALL_TIMEOUT_SECONDS}s, aborting\")\n                process.destroyForcibly()\n                return STATUS_FAILURE\n            }\n\n            val exitCode = process.exitValue()\n            log(\"pm uninstall — exitCode=$exitCode, stdout='$stdout', stderr='$stderr'\")\n\n            if (exitCode == 0 && stdout.contains(\"Success\")) {\n                log(\"Uninstall SUCCESS\")\n                STATUS_SUCCESS\n            } else {\n                logE(\"Uninstall FAILED — exitCode=$exitCode, stdout='$stdout', stderr='$stderr'\")\n                STATUS_FAILURE\n            }\n        } catch (e: Exception) {\n            logE(\"uninstallPackage() exception\", e)\n            STATUS_FAILURE\n        }\n    }\n\n    override fun destroy() {\n        log(\"destroy() — service being unbound\")\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerWrapper.kt",
    "content": "package zed.rainxch.core.data.services.shizuku\n\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.data.services.shizuku.model.ShizukuStatus\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.core.domain.model.SystemArchitecture\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.system.InstallerInfoExtractor\n\n/**\n * Wrapper around [Installer] that transparently intercepts `install()` and `uninstall()`\n * calls to use Shizuku when available and enabled by the user.\n *\n * All other methods (asset selection, architecture detection, Obtainium/AppManager support)\n * delegate directly to the underlying [androidInstaller] unchanged.\n *\n * Fallback behavior: if Shizuku install/uninstall fails for any reason, falls back\n * to the standard [androidInstaller] implementation silently.\n */\nclass ShizukuInstallerWrapper(\n    private val androidInstaller: Installer,\n    private val shizukuServiceManager: ShizukuServiceManager,\n    private val tweaksRepository: TweaksRepository,\n) : Installer {\n    companion object {\n        private const val TAG = \"ShizukuInstaller\"\n    }\n\n    /**\n     * Cached installer type preference, updated by flow collection.\n     * Defaults to DEFAULT so Shizuku is never used unless explicitly opted in.\n     */\n    @Volatile\n    private var cachedInstallerType: InstallerType = InstallerType.DEFAULT\n\n    /**\n     * Start observing the installer type preference from DataStore.\n     * Call once after construction (from DI setup).\n     */\n    fun observeInstallerPreference(scope: CoroutineScope) {\n        scope.launch {\n            tweaksRepository.getInstallerType().collect { type ->\n                cachedInstallerType = type\n                Logger.d(TAG) { \"Installer type changed to: $type\" }\n            }\n        }\n    }\n\n    override suspend fun isSupported(extOrMime: String): Boolean = androidInstaller.isSupported(extOrMime)\n\n    override fun isAssetInstallable(assetName: String): Boolean = androidInstaller.isAssetInstallable(assetName)\n\n    override fun choosePrimaryAsset(assets: List<GithubAsset>): GithubAsset? = androidInstaller.choosePrimaryAsset(assets)\n\n    override fun detectSystemArchitecture(): SystemArchitecture = androidInstaller.detectSystemArchitecture()\n\n    override fun isObtainiumInstalled(): Boolean = androidInstaller.isObtainiumInstalled()\n\n    override fun openInObtainium(\n        repoOwner: String,\n        repoName: String,\n        onOpenInstaller: () -> Unit,\n    ) = androidInstaller.openInObtainium(repoOwner, repoName, onOpenInstaller)\n\n    override fun isAppManagerInstalled(): Boolean = androidInstaller.isAppManagerInstalled()\n\n    override fun openInAppManager(\n        filePath: String,\n        onOpenInstaller: () -> Unit,\n    ) = androidInstaller.openInAppManager(filePath, onOpenInstaller)\n\n    override fun getApkInfoExtractor(): InstallerInfoExtractor = androidInstaller.getApkInfoExtractor()\n\n    override fun openApp(packageName: String): Boolean = androidInstaller.openApp(packageName)\n\n    override fun openWithExternalInstaller(filePath: String) = androidInstaller.openWithExternalInstaller(filePath)\n\n    override suspend fun ensurePermissionsOrThrow(extOrMime: String) {\n        Logger.d(TAG) {\n            \"ensurePermissionsOrThrow() — extOrMime=$extOrMime, cachedType=$cachedInstallerType, status=${shizukuServiceManager.status.value}\"\n        }\n        if (shouldUseShizuku()) {\n            Logger.d(TAG) { \"Shizuku active — skipping unknown sources permission check\" }\n            return\n        }\n        Logger.d(TAG) { \"Delegating ensurePermissionsOrThrow to AndroidInstaller\" }\n        androidInstaller.ensurePermissionsOrThrow(extOrMime)\n    }\n\n    override suspend fun install(\n        filePath: String,\n        extOrMime: String,\n    ) {\n        Logger.d(TAG) { \"install() called — filePath=$filePath, extOrMime=$extOrMime\" }\n        Logger.d(TAG) { \"cachedInstallerType=$cachedInstallerType, shizukuStatus=${shizukuServiceManager.status.value}\" }\n\n        if (shouldUseShizuku()) {\n            Logger.d(TAG) { \"Shizuku is enabled and READY — attempting Shizuku install\" }\n            try {\n                val service = shizukuServiceManager.getService()\n                if (service != null) {\n                    val result =\n                        withContext(Dispatchers.IO) {\n                            val file = java.io.File(filePath)\n                            val pfd =\n                                android.os.ParcelFileDescriptor.open(\n                                    file,\n                                    android.os.ParcelFileDescriptor.MODE_READ_ONLY,\n                                )\n                            pfd.use {\n                                Logger.d(TAG) { \"Got Shizuku service, calling installPackage($filePath, size=${file.length()})...\" }\n                                service.installPackage(it, file.length())\n                            }\n                        }\n                    Logger.d(TAG) { \"Shizuku installPackage() returned: $result\" }\n                    if (result == 0) {\n                        Logger.d(TAG) { \"Shizuku install SUCCEEDED for: $filePath\" }\n                        return\n                    }\n                    Logger.w(TAG) { \"Shizuku install FAILED with code: $result, falling back to standard installer\" }\n                } else {\n                    Logger.w(TAG) { \"Shizuku service is NULL, falling back to standard installer\" }\n                }\n            } catch (e: Exception) {\n                Logger.e(TAG) { \"Shizuku install exception, falling back: ${e.javaClass.simpleName}: ${e.message}\" }\n            }\n        } else {\n            Logger.d(TAG) { \"Not using Shizuku (enabled=${isShizukuEnabled()}, status=${shizukuServiceManager.status.value})\" }\n        }\n\n        Logger.d(TAG) { \"Using standard AndroidInstaller for: $filePath\" }\n        androidInstaller.ensurePermissionsOrThrow(extOrMime)\n        androidInstaller.install(filePath, extOrMime)\n    }\n\n    override fun uninstall(packageName: String) {\n        Logger.d(TAG) { \"uninstall() called — packageName=$packageName\" }\n        Logger.d(TAG) { \"cachedInstallerType=$cachedInstallerType, shizukuStatus=${shizukuServiceManager.status.value}\" }\n\n        if (isShizukuEnabled() && shizukuServiceManager.status.value == ShizukuStatus.READY) {\n            Logger.d(TAG) { \"Attempting Shizuku uninstall...\" }\n            Thread {\n                try {\n                    val service = runBlocking { shizukuServiceManager.getService() }\n                    if (service != null) {\n                        Logger.d(TAG) { \"Got service, calling uninstallPackage($packageName)...\" }\n                        val result = service.uninstallPackage(packageName)\n                        Logger.d(TAG) { \"Shizuku uninstallPackage() returned: $result\" }\n                        if (result == 0) {\n                            Logger.d(TAG) { \"Shizuku uninstall SUCCEEDED for: $packageName\" }\n                        } else {\n                            Logger.w(TAG) { \"Shizuku uninstall FAILED with code: $result, falling back\" }\n                            androidInstaller.uninstall(packageName)\n                        }\n                    } else {\n                        Logger.w(TAG) { \"Shizuku service is NULL, falling back\" }\n                        androidInstaller.uninstall(packageName)\n                    }\n                } catch (e: Exception) {\n                    Logger.e(TAG) { \"Shizuku uninstall exception, falling back: ${e.message}\" }\n                    androidInstaller.uninstall(packageName)\n                }\n            }.start()\n            return\n        }\n\n        Logger.d(TAG) { \"Using standard AndroidInstaller uninstall for: $packageName\" }\n        androidInstaller.uninstall(packageName)\n    }\n\n    private suspend fun shouldUseShizuku(): Boolean = isShizukuEnabled() && shizukuServiceManager.status.value == ShizukuStatus.READY\n\n    private fun isShizukuEnabled(): Boolean = cachedInstallerType == InstallerType.SHIZUKU\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuServiceManager.kt",
    "content": "package zed.rainxch.core.data.services.shizuku\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.ServiceConnection\nimport android.content.pm.PackageManager\nimport android.os.IBinder\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withTimeoutOrNull\nimport rikka.shizuku.Shizuku\nimport zed.rainxch.core.data.services.shizuku.model.ShizukuStatus\nimport kotlin.coroutines.resume\n\nclass ShizukuServiceManager(\n    private val context: Context,\n) {\n    private val _status = MutableStateFlow(ShizukuStatus.NOT_INSTALLED)\n    val status: StateFlow<ShizukuStatus> = _status.asStateFlow()\n\n    private val bindMutex = Mutex()\n    private var serviceConnection: ServiceConnection? = null\n    private var boundUserServiceArgs: Shizuku.UserServiceArgs? = null\n\n    @Volatile\n    var installerService: IShizukuInstallerService? = null\n        private set\n\n    private val binderReceivedListener =\n        Shizuku.OnBinderReceivedListener {\n            Logger.d(TAG) { \"Shizuku binder received\" }\n            refreshStatus()\n        }\n\n    private val binderDeadListener =\n        Shizuku.OnBinderDeadListener {\n            Logger.d(TAG) { \"Shizuku binder dead\" }\n            installerService = null\n            refreshStatus()\n        }\n\n    private val permissionResultListener =\n        Shizuku.OnRequestPermissionResultListener { requestCode, grantResult ->\n            Logger.d(TAG) {\n                \"Shizuku permission result: requestCode=$requestCode,\" +\n                    \" granted=${grantResult == PackageManager.PERMISSION_GRANTED}\"\n            }\n            refreshStatus()\n        }\n\n    fun initialize() {\n        try {\n            Shizuku.addBinderReceivedListenerSticky(binderReceivedListener)\n            Shizuku.addBinderDeadListener(binderDeadListener)\n            Shizuku.addRequestPermissionResultListener(permissionResultListener)\n        } catch (e: Exception) {\n            Logger.w(TAG) { \"Failed to register Shizuku listeners: ${e.message}\" }\n        }\n        refreshStatus()\n    }\n\n    fun refreshStatus() {\n        _status.value = computeStatus()\n    }\n\n    private fun computeStatus(): ShizukuStatus {\n        val installed = isShizukuInstalled()\n        Logger.d(TAG) { \"computeStatus() — shizukuInstalled=$installed\" }\n        if (!installed) return ShizukuStatus.NOT_INSTALLED\n\n        return try {\n            val binderAlive = Shizuku.pingBinder()\n            Logger.d(TAG) { \"computeStatus() — pingBinder=$binderAlive\" }\n            if (!binderAlive) return ShizukuStatus.NOT_RUNNING\n\n            val permResult = Shizuku.checkSelfPermission()\n            Logger.d(TAG) { \"computeStatus() — checkSelfPermission=$permResult (GRANTED=${PackageManager.PERMISSION_GRANTED})\" }\n            if (permResult != PackageManager.PERMISSION_GRANTED) {\n                return ShizukuStatus.PERMISSION_NEEDED\n            }\n            Logger.d(TAG) { \"computeStatus() — READY\" }\n            ShizukuStatus.READY\n        } catch (e: Exception) {\n            Logger.w(TAG) { \"Error checking Shizuku status: ${e.javaClass.simpleName}: ${e.message}\" }\n            ShizukuStatus.NOT_RUNNING\n        }\n    }\n\n    private fun isShizukuInstalled(): Boolean =\n        try {\n            context.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0)\n            true\n        } catch (_: PackageManager.NameNotFoundException) {\n            false\n        } catch (_: Exception) {\n            false\n        }\n\n    fun requestPermission() {\n        try {\n            if (Shizuku.pingBinder()) {\n                Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)\n            }\n        } catch (e: Exception) {\n            Logger.w(TAG) { \"Failed to request Shizuku permission: ${e.message}\" }\n        }\n    }\n\n    suspend fun getService(): IShizukuInstallerService? {\n        Logger.d(TAG) { \"getService() — current status=${_status.value}\" }\n        if (_status.value != ShizukuStatus.READY) {\n            Logger.w(TAG) { \"getService() — Shizuku not READY (status=${_status.value}), returning null\" }\n            return null\n        }\n\n        return bindMutex.withLock {\n            installerService?.let { service ->\n                try {\n                    val alive = service.asBinder().pingBinder()\n                    Logger.d(TAG) { \"getService() — cached service ping=$alive\" }\n                    if (alive) return@withLock service\n                    Logger.w(TAG) { \"getService() — cached service binder dead, rebinding...\" }\n                    installerService = null\n                } catch (e: Exception) {\n                    Logger.w(TAG) { \"getService() — cached service error: ${e.message}, rebinding...\" }\n                    installerService = null\n                }\n            } ?: run {\n                Logger.d(TAG) { \"getService() — no cached service, binding...\" }\n            }\n\n            bindService()\n        }\n    }\n\n    private suspend fun bindService(): IShizukuInstallerService? {\n        Logger.d(TAG) { \"bindService() — attempting to bind Shizuku UserService...\" }\n        return try {\n            withTimeoutOrNull(BIND_TIMEOUT_MS) {\n                suspendCancellableCoroutine { continuation ->\n                    val componentName =\n                        ComponentName(\n                            context.packageName,\n                            ShizukuInstallerServiceImpl::class.java.name,\n                        )\n                    Logger.d(TAG) { \"bindService() — component: $componentName\" }\n\n                    val args =\n                        Shizuku\n                            .UserServiceArgs(componentName)\n                            .daemon(false)\n                            .processNameSuffix(\"installer\")\n                            .version(1)\n\n                    val connection =\n                        object : ServiceConnection {\n                            override fun onServiceConnected(\n                                name: ComponentName?,\n                                binder: IBinder?,\n                            ) {\n                                Logger.d(\n                                    TAG,\n                                ) {\n                                    \"onServiceConnected() — name=$name, binder=${binder?.javaClass?.name}, binderAlive=${binder?.pingBinder()}\"\n                                }\n                                val service = IShizukuInstallerService.Stub.asInterface(binder)\n                                installerService = service\n                                Logger.d(TAG) { \"Shizuku installer service connected and cached\" }\n                                if (continuation.isActive) {\n                                    continuation.resume(service)\n                                }\n                            }\n\n                            override fun onServiceDisconnected(name: ComponentName?) {\n                                installerService = null\n                                Logger.d(TAG) { \"Shizuku installer service disconnected: $name\" }\n                            }\n                        }\n\n                    serviceConnection = connection\n                    boundUserServiceArgs = args\n\n                    Logger.d(TAG) { \"Calling Shizuku.bindUserService()...\" }\n                    Shizuku.bindUserService(args, connection)\n                    Logger.d(TAG) { \"Shizuku.bindUserService() called, waiting for callback...\" }\n\n                    continuation.invokeOnCancellation {\n                        Logger.d(TAG) { \"bindService() coroutine cancelled, unbinding...\" }\n                        try {\n                            Shizuku.unbindUserService(args, connection, true)\n                        } catch (_: Exception) {\n                        }\n                    }\n                }\n            }.also { service ->\n                if (service == null) {\n                    Logger.w(TAG) { \"bindService() timed out after ${BIND_TIMEOUT_MS}ms\" }\n                }\n            }\n        } catch (e: Exception) {\n            Logger.e(TAG) { \"Failed to bind Shizuku service: ${e.javaClass.simpleName}: ${e.message}\" }\n            Logger.e(TAG) { e.stackTraceToString() }\n            null\n        }\n    }\n\n    companion object {\n        private const val TAG = \"ShizukuServiceManager\"\n        private const val SHIZUKU_PACKAGE = \"moe.shizuku.privileged.api\"\n        private const val BIND_TIMEOUT_MS = 15_000L\n        const val SHIZUKU_PERMISSION_REQUEST_CODE = 1001\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/model/ShizukuStatus.kt",
    "content": "package zed.rainxch.core.data.services.shizuku.model\n\nenum class ShizukuStatus {\n    NOT_INSTALLED,\n    NOT_RUNNING,\n    PERMISSION_NEEDED,\n    READY,\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidAppLauncher.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport android.content.Context\nimport android.content.Intent\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.utils.AppLauncher\n\nclass AndroidAppLauncher(\n    private val context: Context,\n    private val logger: GitHubStoreLogger,\n) : AppLauncher {\n    override suspend fun launchApp(installedApp: InstalledApp): Result<Unit> =\n        withContext(Dispatchers.Main) {\n            runCatching {\n                val packageManager = context.packageManager\n\n                val launchIntent = packageManager.getLaunchIntentForPackage(installedApp.packageName)\n\n                if (launchIntent != null) {\n                    launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                    context.startActivity(launchIntent)\n                    logger.debug(\"Launched app: ${installedApp.packageName}\")\n                } else {\n                    throw Exception(\"No launch intent found for ${installedApp.packageName}\")\n                }\n            }.onFailure { error ->\n                logger.error(\"Failed to launch app ${installedApp.packageName}: ${error.message}\")\n            }\n        }\n\n    override suspend fun canLaunchApp(installedApp: InstalledApp): Boolean =\n        withContext(Dispatchers.IO) {\n            try {\n                val packageManager = context.packageManager\n                packageManager.getLaunchIntentForPackage(installedApp.packageName) != null\n            } catch (e: Exception) {\n                false\n            }\n        }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidBrowserHelper.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.core.net.toUri\nimport zed.rainxch.core.domain.utils.BrowserHelper\n\nclass AndroidBrowserHelper(\n    private val context: Context,\n) : BrowserHelper {\n    override fun openUrl(\n        url: String,\n        onFailure: (error: String) -> Unit,\n    ) {\n        val intent =\n            Intent(Intent.ACTION_VIEW, url.toUri()).apply {\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n        context.startActivity(intent)\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidClipboardHelper.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport zed.rainxch.core.domain.utils.ClipboardHelper\n\nclass AndroidClipboardHelper(\n    private val context: Context,\n) : ClipboardHelper {\n    override fun copy(\n        label: String,\n        text: String,\n    ) {\n        val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n        cm.setPrimaryClip(ClipData.newPlainText(label, text))\n    }\n\n    override fun getText(): String? {\n        val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n        val clip = cm.primaryClip ?: return null\n        if (clip.itemCount == 0) return null\n        return clip.getItemAt(0).text?.toString()\n    }\n}\n"
  },
  {
    "path": "core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidShareManager.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.activity.ComponentActivity\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.content.FileProvider\nimport zed.rainxch.core.domain.utils.ShareManager\nimport java.io.File\n\nclass AndroidShareManager(\n    private val context: Context,\n) : ShareManager {\n    private var filePickerCallback: ((String?) -> Unit)? = null\n    private var filePickerLauncher: ActivityResultLauncher<Intent>? = null\n\n    fun registerActivityResultLauncher(activity: ComponentActivity) {\n        filePickerLauncher = activity.registerForActivityResult(\n            ActivityResultContracts.StartActivityForResult()\n        ) { result ->\n            val callback = filePickerCallback\n            filePickerCallback = null\n\n            if (result.resultCode == Activity.RESULT_OK) {\n                val uri = result.data?.data\n                if (uri != null) {\n                    try {\n                        val content = context.contentResolver.openInputStream(uri)\n                            ?.bufferedReader()\n                            ?.use { it.readText() }\n                        callback?.invoke(content)\n                    } catch (e: Exception) {\n                        callback?.invoke(null)\n                    }\n                } else {\n                    callback?.invoke(null)\n                }\n            } else {\n                callback?.invoke(null)\n            }\n        }\n    }\n\n    override fun shareText(text: String) {\n        val intent =\n            Intent(Intent.ACTION_SEND).apply {\n                type = \"text/plain\"\n                putExtra(Intent.EXTRA_TEXT, text)\n            }\n\n        val chooser =\n            Intent.createChooser(intent, null).apply {\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n\n        context.startActivity(chooser)\n    }\n\n    override fun shareFile(fileName: String, content: String, mimeType: String) {\n        val cacheDir = File(context.cacheDir, \"exports\")\n        cacheDir.mkdirs()\n\n        val file = File(cacheDir, fileName)\n        file.writeText(content)\n\n        val uri: Uri = FileProvider.getUriForFile(\n            context,\n            \"${context.packageName}.fileprovider\",\n            file\n        )\n\n        val intent = Intent(Intent.ACTION_SEND).apply {\n            type = mimeType\n            putExtra(Intent.EXTRA_STREAM, uri)\n            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        }\n\n        val chooser = Intent.createChooser(intent, null).apply {\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        }\n\n        context.startActivity(chooser)\n    }\n\n    override fun pickFile(mimeType: String, onResult: (String?) -> Unit) {\n        filePickerCallback = onResult\n\n        val launcher = filePickerLauncher\n        if (launcher != null) {\n            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {\n                addCategory(Intent.CATEGORY_OPENABLE)\n                type = mimeType\n            }\n            launcher.launch(intent)\n        } else {\n            // Fallback: try with ACTION_GET_CONTENT\n            val intent = Intent(Intent.ACTION_GET_CONTENT).apply {\n                addCategory(Intent.CATEGORY_OPENABLE)\n                type = mimeType\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n            try {\n                context.startActivity(Intent.createChooser(intent, null).apply {\n                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                })\n                // Note: fallback won't deliver result without launcher\n                onResult(null)\n            } catch (e: Exception) {\n                onResult(null)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt",
    "content": "package zed.rainxch.core.data.cache\n\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.serializer\nimport zed.rainxch.core.data.local.db.dao.CacheDao\nimport zed.rainxch.core.data.local.db.entities.CacheEntryEntity\nimport kotlin.time.Clock\nimport kotlin.time.Duration.Companion.hours\n\nclass CacheManager(\n    val cacheDao: CacheDao,\n) {\n    val json =\n        Json {\n            ignoreUnknownKeys = true\n            isLenient = true\n            encodeDefaults = true\n        }\n\n    val memoryCache = HashMap<String, Pair<Long, String>>()\n\n    fun now(): Long = Clock.System.now().toEpochMilliseconds()\n\n    suspend inline fun <reified T> get(key: String): T? {\n        val currentTime = now()\n\n        memoryCache[key]?.let { (expiresAt, jsonData) ->\n            if (expiresAt > currentTime) {\n                return try {\n                    json.decodeFromString(serializer<T>(), jsonData)\n                } catch (_: Exception) {\n                    memoryCache.remove(key)\n                    null\n                }\n            } else {\n                memoryCache.remove(key)\n            }\n        }\n\n        val entry = cacheDao.getValid(key, currentTime) ?: return null\n        memoryCache[key] = entry.expiresAt to entry.jsonData\n\n        return try {\n            json.decodeFromString(serializer<T>(), entry.jsonData)\n        } catch (_: Exception) {\n            cacheDao.delete(key)\n            memoryCache.remove(key)\n            null\n        }\n    }\n\n    suspend inline fun <reified T> getStale(key: String): T? {\n        val entry = cacheDao.getAny(key) ?: return null\n        return try {\n            json.decodeFromString(serializer<T>(), entry.jsonData)\n        } catch (_: Exception) {\n            null\n        }\n    }\n\n    suspend inline fun <reified T> put(\n        key: String,\n        value: T,\n        ttlMillis: Long,\n    ) {\n        val currentTime = now()\n        val jsonData = json.encodeToString(serializer<T>(), value)\n        val expiresAt = currentTime + ttlMillis\n\n        memoryCache[key] = expiresAt to jsonData\n\n        cacheDao.put(\n            CacheEntryEntity(\n                key = key,\n                jsonData = jsonData,\n                cachedAt = currentTime,\n                expiresAt = expiresAt,\n            ),\n        )\n    }\n\n    suspend fun invalidate(key: String) {\n        memoryCache.remove(key)\n        cacheDao.delete(key)\n    }\n\n    suspend fun invalidateByPrefix(prefix: String) {\n        val keysToRemove = memoryCache.keys.filter { it.startsWith(prefix) }\n        keysToRemove.forEach { memoryCache.remove(it) }\n        cacheDao.deleteByPrefix(prefix)\n    }\n\n    suspend fun clearAll() {\n        memoryCache.clear()\n        cacheDao.deleteAll()\n    }\n\n    suspend fun cleanupExpired() {\n        val currentTime = now()\n        val expiredKeys =\n            memoryCache.entries\n                .filter { it.value.first <= currentTime }\n                .map { it.key }\n        expiredKeys.forEach { memoryCache.remove(it) }\n        cacheDao.deleteExpired(currentTime)\n    }\n\n    companion object CacheTtl {\n        val HOME_REPOS = 12.hours.inWholeMilliseconds\n        val REPO_DETAILS = 6.hours.inWholeMilliseconds\n        val RELEASES = 6.hours.inWholeMilliseconds\n        val README = 12.hours.inWholeMilliseconds\n        val USER_PROFILE = 6.hours.inWholeMilliseconds\n        val SEARCH_RESULTS = 1.hours.inWholeMilliseconds\n        val REPO_STATS = 6.hours.inWholeMilliseconds\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/data_source/TokenStore.kt",
    "content": "package zed.rainxch.core.data.data_source\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.data.dto.GithubDeviceTokenSuccessDto\n\ninterface TokenStore {\n    fun tokenFlow(): Flow<GithubDeviceTokenSuccessDto?>\n\n    suspend fun currentToken(): GithubDeviceTokenSuccessDto?\n\n    fun blockingCurrentToken(): GithubDeviceTokenSuccessDto?\n\n    suspend fun save(token: GithubDeviceTokenSuccessDto)\n\n    suspend fun clear()\n\n    suspend fun isTokenExpired(): Boolean\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/data_source/impl/DefaultTokenStore.kt",
    "content": "package zed.rainxch.core.data.data_source.impl\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.edit\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.serialization.json.Json\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.data.dto.GithubDeviceTokenSuccessDto\nimport kotlin.time.Clock\n\nclass DefaultTokenStore(\n    private val dataStore: DataStore<Preferences>,\n) : TokenStore {\n    private val TOKEN_KEY = stringPreferencesKey(\"token\")\n    private val json = Json { ignoreUnknownKeys = true }\n\n    override suspend fun save(token: GithubDeviceTokenSuccessDto) {\n        val stamped =\n            token.copy(\n                savedAtEpochMillis = token.savedAtEpochMillis ?: Clock.System.now().toEpochMilliseconds(),\n            )\n        val jsonString = json.encodeToString(GithubDeviceTokenSuccessDto.serializer(), stamped)\n        dataStore.edit { preferences ->\n            preferences[TOKEN_KEY] = jsonString\n        }\n    }\n\n    override fun tokenFlow(): Flow<GithubDeviceTokenSuccessDto?> {\n        return dataStore.data.map { preferences ->\n            val raw = preferences[TOKEN_KEY] ?: return@map null\n            runCatching {\n                json.decodeFromString(GithubDeviceTokenSuccessDto.serializer(), raw)\n            }.getOrNull()\n        }\n    }\n\n    override suspend fun currentToken(): GithubDeviceTokenSuccessDto? {\n        val preferences = dataStore.data.first()\n        val raw = preferences[TOKEN_KEY] ?: return null\n        return runCatching {\n            json.decodeFromString(GithubDeviceTokenSuccessDto.serializer(), raw)\n        }.getOrNull()\n    }\n\n    override fun blockingCurrentToken(): GithubDeviceTokenSuccessDto? =\n        runBlocking {\n            val preferences = dataStore.data.first()\n            val raw = preferences[TOKEN_KEY] ?: return@runBlocking null\n            runCatching {\n                json.decodeFromString(GithubDeviceTokenSuccessDto.serializer(), raw)\n            }.getOrNull()\n        }\n\n    override suspend fun clear() {\n        dataStore.edit { it.remove(TOKEN_KEY) }\n    }\n\n    override suspend fun isTokenExpired(): Boolean {\n        val token = currentToken() ?: return true\n        val savedAt = token.savedAtEpochMillis ?: return false\n        val expiresIn = token.expiresIn ?: return false\n        val expiresAtMillis = savedAt + (expiresIn * 1000L)\n        return Clock.System.now().toEpochMilliseconds() > expiresAtMillis\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/PlatformModule.kt",
    "content": "package zed.rainxch.core.data.di\n\nimport org.koin.core.module.Module\n\nexpect val corePlatformModule: Module\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt",
    "content": "package zed.rainxch.core.data.di\n\nimport io.ktor.client.HttpClient\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withTimeout\nimport org.koin.dsl.module\nimport zed.rainxch.core.data.cache.CacheManager\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.data.data_source.impl.DefaultTokenStore\nimport zed.rainxch.core.data.local.db.AppDatabase\nimport zed.rainxch.core.data.local.db.dao.CacheDao\nimport zed.rainxch.core.data.local.db.dao.FavoriteRepoDao\nimport zed.rainxch.core.data.local.db.dao.InstalledAppDao\nimport zed.rainxch.core.data.local.db.dao.SeenRepoDao\nimport zed.rainxch.core.data.local.db.dao.StarredRepoDao\nimport zed.rainxch.core.data.local.db.dao.UpdateHistoryDao\nimport zed.rainxch.core.data.logging.KermitLogger\nimport zed.rainxch.core.data.network.GitHubClientProvider\nimport zed.rainxch.core.data.network.ProxyManager\nimport zed.rainxch.core.data.network.createGitHubHttpClient\nimport zed.rainxch.core.data.repository.AuthenticationStateImpl\nimport zed.rainxch.core.data.repository.FavouritesRepositoryImpl\nimport zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl\nimport zed.rainxch.core.data.repository.ProxyRepositoryImpl\nimport zed.rainxch.core.data.repository.RateLimitRepositoryImpl\nimport zed.rainxch.core.data.repository.SeenReposRepositoryImpl\nimport zed.rainxch.core.data.repository.StarredRepositoryImpl\nimport zed.rainxch.core.data.repository.TweaksRepositoryImpl\nimport zed.rainxch.core.domain.getPlatform\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.repository.AuthenticationState\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.ProxyRepository\nimport zed.rainxch.core.domain.repository.RateLimitRepository\nimport zed.rainxch.core.domain.repository.SeenReposRepository\nimport zed.rainxch.core.domain.repository.StarredRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\n\nval coreModule =\n    module {\n        single {\n            CoroutineScope(Dispatchers.IO + SupervisorJob())\n        }\n\n        single<GitHubStoreLogger> {\n            KermitLogger\n        }\n\n        single<Platform> {\n            getPlatform()\n        }\n\n        single<AuthenticationState> {\n            AuthenticationStateImpl(\n                tokenStore = get(),\n            )\n        }\n\n        single<FavouritesRepository> {\n            FavouritesRepositoryImpl(\n                favoriteRepoDao = get(),\n                installedAppsDao = get(),\n            )\n        }\n\n        single<InstalledAppsRepository> {\n            InstalledAppsRepositoryImpl(\n                database = get(),\n                installedAppsDao = get(),\n                historyDao = get(),\n                installer = get(),\n                httpClient = get(),\n                tweaksRepository = get(),\n            )\n        }\n\n        single<StarredRepository> {\n            StarredRepositoryImpl(\n                installedAppsDao = get(),\n                starredRepoDao = get(),\n                platform = get(),\n                httpClient = get(),\n            )\n        }\n\n        single<TweaksRepository> {\n            TweaksRepositoryImpl(\n                preferences = get(),\n            )\n        }\n\n        single<SeenReposRepository> {\n            SeenReposRepositoryImpl(\n                seenRepoDao = get(),\n            )\n        }\n\n        single<ProxyRepository> {\n            ProxyRepositoryImpl(\n                preferences = get(),\n            )\n        }\n\n        single<SyncInstalledAppsUseCase> {\n            SyncInstalledAppsUseCase(\n                packageMonitor = get(),\n                installedAppsRepository = get(),\n                platform = get(),\n                logger = get(),\n            )\n        }\n\n        single<CacheManager> {\n            CacheManager(cacheDao = get())\n        }\n    }\n\nval networkModule =\n    module {\n        single<GitHubClientProvider> {\n            val config =\n                runBlocking {\n                    runCatching {\n                        withTimeout(1_500L) {\n                            get<ProxyRepository>().getProxyConfig().first()\n                        }\n                    }.getOrDefault(ProxyConfig.None)\n                }\n\n            when (config) {\n                is ProxyConfig.None -> {\n                    ProxyManager.setNoProxy()\n                }\n\n                is ProxyConfig.System -> {\n                    ProxyManager.setSystemProxy()\n                }\n\n                is ProxyConfig.Http -> {\n                    ProxyManager.setHttpProxy(\n                        host = config.host,\n                        port = config.port,\n                        username = config.username,\n                        password = config.password,\n                    )\n                }\n\n                is ProxyConfig.Socks -> {\n                    ProxyManager.setSocksProxy(\n                        host = config.host,\n                        port = config.port,\n                        username = config.username,\n                        password = config.password,\n                    )\n                }\n            }\n\n            GitHubClientProvider(\n                tokenStore = get(),\n                rateLimitRepository = get(),\n                authenticationState = get(),\n                proxyConfigFlow = ProxyManager.currentProxyConfig,\n            )\n        }\n\n        single<HttpClient> {\n            createGitHubHttpClient(\n                tokenStore = get(),\n                rateLimitRepository = get(),\n                authenticationState = get(),\n                scope = get(),\n            )\n        }\n\n        single<TokenStore> {\n            DefaultTokenStore(\n                dataStore = get(),\n            )\n        }\n\n        single<RateLimitRepository> {\n            RateLimitRepositoryImpl()\n        }\n    }\n\nval databaseModule =\n    module {\n        single<FavoriteRepoDao> {\n            get<AppDatabase>().favoriteRepoDao\n        }\n\n        single<InstalledAppDao> {\n            get<AppDatabase>().installedAppDao\n        }\n\n        single<StarredRepoDao> {\n            get<AppDatabase>().starredReposDao\n        }\n\n        single<UpdateHistoryDao> {\n            get<AppDatabase>().updateHistoryDao\n        }\n\n        single<CacheDao> {\n            get<AppDatabase>().cacheDao\n        }\n\n        single<SeenRepoDao> {\n            get<AppDatabase>().seenRepoDao\n        }\n    }\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AssetNetwork(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"name\") val name: String,\n    @SerialName(\"content_type\") val contentType: String,\n    @SerialName(\"size\") val size: Long,\n    @SerialName(\"browser_download_url\") val downloadUrl: String,\n    @SerialName(\"uploader\") val uploader: OwnerNetwork,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GitHubStarredResponse.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GitHubStarredResponse(\n    val id: Long,\n    val name: String,\n    val owner: Owner,\n    val description: String?,\n    val language: String?,\n    @SerialName(\"html_url\") val htmlUrl: String,\n    @SerialName(\"stargazers_count\") val stargazersCount: Int,\n    @SerialName(\"forks_count\") val forksCount: Int,\n    @SerialName(\"open_issues_count\") val openIssuesCount: Int,\n    @SerialName(\"starred_at\") val starredAt: String? = null,\n) {\n    @Serializable\n    data class Owner(\n        val login: String,\n        @SerialName(\"avatar_url\") val avatarUrl: String,\n    )\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubDeviceStartDto.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubDeviceStartDto(\n    @SerialName(\"device_code\") val deviceCode: String,\n    @SerialName(\"user_code\") val userCode: String,\n    @SerialName(\"verification_uri\") val verificationUri: String,\n    @SerialName(\"verification_uri_complete\") val verificationUriComplete: String? = null,\n    @SerialName(\"interval\") val intervalSec: Int = 5,\n    @SerialName(\"expires_in\") val expiresInSec: Int,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubDeviceTokenErrorDto.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubDeviceTokenErrorDto(\n    @SerialName(\"error\") val error: String,\n    @SerialName(\"error_description\") val errorDescription: String? = null,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubDeviceTokenSuccessDto.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubDeviceTokenSuccessDto(\n    @SerialName(\"access_token\") val accessToken: String,\n    @SerialName(\"token_type\") val tokenType: String,\n    @SerialName(\"expires_in\") val expiresIn: Long? = null,\n    @SerialName(\"scope\") val scope: String? = null,\n    @SerialName(\"refresh_token\") val refreshToken: String? = null,\n    @SerialName(\"refresh_token_expires_in\") val refreshTokenExpiresIn: Long? = null,\n    @SerialName(\"saved_at\") val savedAtEpochMillis: Long? = null,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubOwnerNetworkModel.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubOwnerNetworkModel(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"login\") val login: String,\n    @SerialName(\"avatar_url\") val avatarUrl: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubRepoNetworkModel.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubRepoNetworkModel(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"name\") val name: String,\n    @SerialName(\"full_name\") val fullName: String,\n    @SerialName(\"owner\") val owner: GithubOwnerNetworkModel,\n    @SerialName(\"description\") val description: String? = null,\n    @SerialName(\"default_branch\") val defaultBranch: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n    @SerialName(\"stargazers_count\") val stargazersCount: Int,\n    @SerialName(\"forks_count\") val forksCount: Int,\n    @SerialName(\"language\") val language: String? = null,\n    @SerialName(\"topics\") val topics: List<String>? = null,\n    @SerialName(\"releases_url\") val releasesUrl: String,\n    @SerialName(\"updated_at\") val updatedAt: String,\n    @SerialName(\"fork\") val fork: Boolean = false,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubRepoSearchResponse.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubRepoSearchResponse(\n    @SerialName(\"total_count\") val totalCount: Int,\n    @SerialName(\"items\") val items: List<GithubRepoNetworkModel>,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/OwnerNetwork.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class OwnerNetwork(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"login\") val login: String,\n    @SerialName(\"avatar_url\") val avatarUrl: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/ReleaseNetwork.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ReleaseNetwork(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"tag_name\") val tagName: String,\n    @SerialName(\"name\") val name: String? = null,\n    @SerialName(\"draft\") val draft: Boolean? = null,\n    @SerialName(\"prerelease\") val prerelease: Boolean? = null,\n    @SerialName(\"author\") val author: OwnerNetwork,\n    @SerialName(\"published_at\") val publishedAt: String? = null,\n    @SerialName(\"created_at\") val createdAt: String? = null,\n    @SerialName(\"body\") val body: String? = null,\n    @SerialName(\"tarball_url\") val tarballUrl: String,\n    @SerialName(\"zipball_url\") val zipballUrl: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n    @SerialName(\"assets\") val assets: List<AssetNetwork>,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoByIdNetwork.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RepoByIdNetwork(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"name\") val name: String,\n    @SerialName(\"full_name\") val fullName: String,\n    @SerialName(\"owner\") val owner: OwnerNetwork,\n    @SerialName(\"description\") val description: String? = null,\n    @SerialName(\"default_branch\") val defaultBranch: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n    @SerialName(\"stargazers_count\") val stars: Int,\n    @SerialName(\"forks_count\") val forks: Int,\n    @SerialName(\"language\") val language: String? = null,\n    @SerialName(\"topics\") val topics: List<String>? = null,\n    @SerialName(\"updated_at\") val updatedAt: String,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RepoInfoNetwork(\n    @SerialName(\"stargazers_count\") val stars: Int,\n    @SerialName(\"forks_count\") val forks: Int,\n    @SerialName(\"open_issues_count\") val openIssues: Int,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/UserProfileNetwork.kt",
    "content": "package zed.rainxch.core.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UserProfileNetwork(\n    @SerialName(\"id\") val id: Long,\n    @SerialName(\"login\") val login: String,\n    @SerialName(\"name\") val name: String? = null,\n    @SerialName(\"bio\") val bio: String? = null,\n    @SerialName(\"avatar_url\") val avatarUrl: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n    @SerialName(\"followers\") val followers: Int,\n    @SerialName(\"following\") val following: Int,\n    @SerialName(\"public_repos\") val publicRepos: Int,\n    @SerialName(\"location\") val location: String? = null,\n    @SerialName(\"company\") val company: String? = null,\n    @SerialName(\"blog\") val blog: String? = null,\n    @SerialName(\"twitter_username\") val twitterUsername: String? = null,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/data_store/createDataStoreCore.kt",
    "content": "package zed.rainxch.core.data.local.data_store\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.PreferenceDataStoreFactory\nimport androidx.datastore.preferences.core.Preferences\nimport okio.Path.Companion.toPath\n\nfun createDataStore(producePath: () -> String): DataStore<Preferences> =\n    PreferenceDataStoreFactory.createWithPath(\n        produceFile = { producePath().toPath() },\n    )\n\ninternal const val dataStoreFileName = \"github_store.preferences_pb\"\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt",
    "content": "package zed.rainxch.core.data.local.db\n\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\nimport zed.rainxch.core.data.local.db.dao.CacheDao\nimport zed.rainxch.core.data.local.db.dao.FavoriteRepoDao\nimport zed.rainxch.core.data.local.db.dao.InstalledAppDao\nimport zed.rainxch.core.data.local.db.dao.SeenRepoDao\nimport zed.rainxch.core.data.local.db.dao.StarredRepoDao\nimport zed.rainxch.core.data.local.db.dao.UpdateHistoryDao\nimport zed.rainxch.core.data.local.db.entities.CacheEntryEntity\nimport zed.rainxch.core.data.local.db.entities.FavoriteRepoEntity\nimport zed.rainxch.core.data.local.db.entities.InstalledAppEntity\nimport zed.rainxch.core.data.local.db.entities.SeenRepoEntity\nimport zed.rainxch.core.data.local.db.entities.StarredRepositoryEntity\nimport zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity\n\n@Database(\n    entities = [\n        InstalledAppEntity::class,\n        FavoriteRepoEntity::class,\n        UpdateHistoryEntity::class,\n        StarredRepositoryEntity::class,\n        CacheEntryEntity::class,\n        SeenRepoEntity::class,\n    ],\n    version = 6,\n    exportSchema = true,\n)\nabstract class AppDatabase : RoomDatabase() {\n    abstract val installedAppDao: InstalledAppDao\n    abstract val favoriteRepoDao: FavoriteRepoDao\n    abstract val updateHistoryDao: UpdateHistoryDao\n    abstract val starredReposDao: StarredRepoDao\n    abstract val cacheDao: CacheDao\n    abstract val seenRepoDao: SeenRepoDao\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/CacheDao.kt",
    "content": "package zed.rainxch.core.data.local.db.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport zed.rainxch.core.data.local.db.entities.CacheEntryEntity\n\n@Dao\ninterface CacheDao {\n    @Query(\"SELECT * FROM cache_entries WHERE `key` = :key AND expiresAt > :now LIMIT 1\")\n    suspend fun getValid(\n        key: String,\n        now: Long,\n    ): CacheEntryEntity?\n\n    @Query(\"SELECT * FROM cache_entries WHERE `key` = :key LIMIT 1\")\n    suspend fun getAny(key: String): CacheEntryEntity?\n\n    @Query(\"SELECT * FROM cache_entries WHERE `key` LIKE :prefix || '%' AND expiresAt > :now\")\n    suspend fun getValidByPrefix(\n        prefix: String,\n        now: Long,\n    ): List<CacheEntryEntity>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun put(entry: CacheEntryEntity)\n\n    @Query(\"DELETE FROM cache_entries WHERE `key` = :key\")\n    suspend fun delete(key: String)\n\n    @Query(\"DELETE FROM cache_entries WHERE `key` LIKE :prefix || '%'\")\n    suspend fun deleteByPrefix(prefix: String)\n\n    @Query(\"DELETE FROM cache_entries WHERE expiresAt <= :now\")\n    suspend fun deleteExpired(now: Long)\n\n    @Query(\"DELETE FROM cache_entries\")\n    suspend fun deleteAll()\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/FavoriteRepoDao.kt",
    "content": "package zed.rainxch.core.data.local.db.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.data.local.db.entities.FavoriteRepoEntity\n\n@Dao\ninterface FavoriteRepoDao {\n    @Query(\"SELECT * FROM favorite_repos ORDER BY addedAt DESC\")\n    fun getAllFavorites(): Flow<List<FavoriteRepoEntity>>\n\n    @Query(\"SELECT * FROM favorite_repos WHERE repoId = :repoId\")\n    suspend fun getFavoriteById(repoId: Long): FavoriteRepoEntity?\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM favorite_repos WHERE repoId = :repoId)\")\n    fun isFavorite(repoId: Long): Flow<Boolean>\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM favorite_repos WHERE repoId = :repoId)\")\n    suspend fun isFavoriteSync(repoId: Long): Boolean\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertFavorite(repo: FavoriteRepoEntity)\n\n    @Delete\n    suspend fun deleteFavorite(repo: FavoriteRepoEntity)\n\n    @Query(\"DELETE FROM favorite_repos WHERE repoId = :repoId\")\n    suspend fun deleteFavoriteById(repoId: Long)\n\n    @Query(\n        \"\"\"\n        UPDATE favorite_repos \n        SET isInstalled = :installed, \n            installedPackageName = :packageName \n        WHERE repoId = :repoId\n    \"\"\",\n    )\n    suspend fun updateInstallStatus(\n        repoId: Long,\n        installed: Boolean,\n        packageName: String?,\n    )\n\n    @Query(\n        \"\"\"\n        UPDATE favorite_repos \n        SET latestVersion = :version,\n            latestReleaseUrl = :releaseUrl,\n            lastSyncedAt = :timestamp\n        WHERE repoId = :repoId\n    \"\"\",\n    )\n    suspend fun updateLatestVersion(\n        repoId: Long,\n        version: String?,\n        releaseUrl: String?,\n        timestamp: Long,\n    )\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt",
    "content": "package zed.rainxch.core.data.local.db.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.Update\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.data.local.db.entities.InstalledAppEntity\n\n@Dao\ninterface InstalledAppDao {\n    @Query(\"SELECT * FROM installed_apps ORDER BY installedAt DESC\")\n    fun getAllInstalledApps(): Flow<List<InstalledAppEntity>>\n\n    @Query(\"SELECT * FROM installed_apps WHERE isUpdateAvailable = 1 ORDER BY lastCheckedAt DESC\")\n    fun getAppsWithUpdates(): Flow<List<InstalledAppEntity>>\n\n    @Query(\"SELECT * FROM installed_apps WHERE packageName = :packageName\")\n    suspend fun getAppByPackage(packageName: String): InstalledAppEntity?\n\n    @Query(\"SELECT * FROM installed_apps WHERE repoId = :repoId\")\n    suspend fun getAppByRepoId(repoId: Long): InstalledAppEntity?\n\n    @Query(\"SELECT * FROM installed_apps WHERE repoId = :repoId\")\n    fun getAppByRepoIdAsFlow(repoId: Long): Flow<InstalledAppEntity?>\n\n    @Query(\"SELECT COUNT(*) FROM installed_apps WHERE isUpdateAvailable = 1\")\n    fun getUpdateCount(): Flow<Int>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertApp(app: InstalledAppEntity)\n\n    @Update\n    suspend fun updateApp(app: InstalledAppEntity)\n\n    @Delete\n    suspend fun deleteApp(app: InstalledAppEntity)\n\n    @Query(\"DELETE FROM installed_apps WHERE packageName = :packageName\")\n    suspend fun deleteByPackageName(packageName: String)\n\n    @Query(\n        \"\"\"\n    UPDATE installed_apps \n    SET isUpdateAvailable = :available, \n        latestVersion = :version,\n        latestAssetName = :assetName,\n        latestAssetUrl = :assetUrl,\n        latestAssetSize = :assetSize,\n        releaseNotes = :releaseNotes,\n        lastCheckedAt = :timestamp,\n        latestVersionName = :latestVersionName,\n        latestVersionCode = :latestVersionCode\n    WHERE packageName = :packageName\n\"\"\",\n    )\n    suspend fun updateVersionInfo(\n        packageName: String,\n        available: Boolean,\n        version: String?,\n        assetName: String?,\n        assetUrl: String?,\n        assetSize: Long?,\n        releaseNotes: String?,\n        timestamp: Long,\n        latestVersionName: String?,\n        latestVersionCode: Long?,\n    )\n\n    @Query(\"UPDATE installed_apps SET lastCheckedAt = :timestamp WHERE packageName = :packageName\")\n    suspend fun updateLastChecked(\n        packageName: String,\n        timestamp: Long,\n    )\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/SeenRepoDao.kt",
    "content": "package zed.rainxch.core.data.local.db.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.data.local.db.entities.SeenRepoEntity\n\n@Dao\ninterface SeenRepoDao {\n    @Query(\"SELECT repoId FROM seen_repos\")\n    fun getAllSeenRepoIds(): Flow<List<Long>>\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    suspend fun insert(entity: SeenRepoEntity)\n\n    @Query(\"DELETE FROM seen_repos\")\n    suspend fun clearAll()\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/StarredRepoDao.kt",
    "content": "package zed.rainxch.core.data.local.db.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.data.local.db.entities.StarredRepositoryEntity\n\n@Dao\ninterface StarredRepoDao {\n    @Query(\"SELECT * FROM starred_repos ORDER BY starredAt DESC\")\n    fun getAllStarred(): Flow<List<StarredRepositoryEntity>>\n\n    @Query(\"SELECT * FROM starred_repos WHERE repoId = :repoId\")\n    suspend fun getStarredById(repoId: Long): StarredRepositoryEntity?\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM starred_repos WHERE repoId = :repoId)\")\n    suspend fun isStarred(repoId: Long): Boolean\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM starred_repos WHERE repoId = :repoId)\")\n    fun isStarredFlow(repoId: Long): Flow<Boolean>\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM starred_repos WHERE repoId = :repoId)\")\n    suspend fun isStarredSync(repoId: Long): Boolean\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertStarred(repo: StarredRepositoryEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAllStarred(repos: List<StarredRepositoryEntity>)\n\n    @Query(\"DELETE FROM starred_repos WHERE repoId = :repoId\")\n    suspend fun deleteStarredById(repoId: Long)\n\n    @Query(\"DELETE FROM starred_repos\")\n    suspend fun clearAll()\n\n    @Query(\n        \"\"\"\n        UPDATE starred_repos \n        SET isInstalled = :installed, \n            installedPackageName = :packageName \n        WHERE repoId = :repoId\n    \"\"\",\n    )\n    suspend fun updateInstallStatus(\n        repoId: Long,\n        installed: Boolean,\n        packageName: String?,\n    )\n\n    @Query(\n        \"\"\"\n        UPDATE starred_repos \n        SET latestVersion = :version,\n            latestReleaseUrl = :releaseUrl,\n            lastSyncedAt = :timestamp\n        WHERE repoId = :repoId\n    \"\"\",\n    )\n    suspend fun updateLatestVersion(\n        repoId: Long,\n        version: String?,\n        releaseUrl: String?,\n        timestamp: Long,\n    )\n\n    @Query(\"SELECT COUNT(*) FROM starred_repos\")\n    suspend fun getCount(): Int\n\n    @Query(\"SELECT MAX(lastSyncedAt) FROM starred_repos\")\n    suspend fun getLastSyncTime(): Long?\n\n    @Transaction\n    suspend fun replaceAllStarred(repos: List<StarredRepositoryEntity>) {\n        clearAll()\n        insertAllStarred(repos)\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/UpdateHistoryDao.kt",
    "content": "package zed.rainxch.core.data.local.db.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.Query\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity\n\n@Dao\ninterface UpdateHistoryDao {\n    @Query(\"SELECT * FROM update_history ORDER BY updatedAt DESC LIMIT 50\")\n    fun getRecentHistory(): Flow<List<UpdateHistoryEntity>>\n\n    @Query(\"SELECT * FROM update_history WHERE packageName = :packageName ORDER BY updatedAt DESC\")\n    fun getHistoryForApp(packageName: String): Flow<List<UpdateHistoryEntity>>\n\n    @Insert\n    suspend fun insertHistory(history: UpdateHistoryEntity)\n\n    @Query(\"DELETE FROM update_history WHERE updatedAt < :timestamp\")\n    suspend fun deleteOldHistory(timestamp: Long)\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/CacheEntryEntity.kt",
    "content": "package zed.rainxch.core.data.local.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"cache_entries\")\ndata class CacheEntryEntity(\n    @PrimaryKey\n    val key: String,\n    val jsonData: String,\n    val cachedAt: Long,\n    val expiresAt: Long,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/FavoriteRepoEntity.kt",
    "content": "package zed.rainxch.core.data.local.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"favorite_repos\")\ndata class FavoriteRepoEntity(\n    @PrimaryKey\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val isInstalled: Boolean = false,\n    val installedPackageName: String? = null,\n    val latestVersion: String?,\n    val latestReleaseUrl: String?,\n    val addedAt: Long,\n    val lastSyncedAt: Long,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt",
    "content": "package zed.rainxch.core.data.local.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport zed.rainxch.core.domain.model.InstallSource\n\n@Entity(tableName = \"installed_apps\")\ndata class InstalledAppEntity(\n    @PrimaryKey val packageName: String,\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val installedVersion: String,\n    val installedAssetName: String?,\n    val installedAssetUrl: String?,\n    val latestVersion: String?,\n    val latestAssetName: String?,\n    val latestAssetUrl: String?,\n    val latestAssetSize: Long?,\n    val appName: String,\n    val installSource: InstallSource,\n    val signingFingerprint: String?,\n    val installedAt: Long,\n    val lastCheckedAt: Long,\n    val lastUpdatedAt: Long,\n    val isUpdateAvailable: Boolean,\n    val updateCheckEnabled: Boolean = true,\n    val releaseNotes: String? = \"\",\n    val systemArchitecture: String,\n    val fileExtension: String,\n    val isPendingInstall: Boolean = false,\n    val installedVersionName: String? = null,\n    val installedVersionCode: Long = 0L,\n    val latestVersionName: String? = null,\n    val latestVersionCode: Long? = null,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/SeenRepoEntity.kt",
    "content": "package zed.rainxch.core.data.local.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"seen_repos\")\ndata class SeenRepoEntity(\n    @PrimaryKey\n    val repoId: Long,\n    val seenAt: Long,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/StarredRepositoryEntity.kt",
    "content": "package zed.rainxch.core.data.local.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"starred_repos\")\ndata class StarredRepositoryEntity(\n    @PrimaryKey\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val openIssuesCount: Int,\n    val isInstalled: Boolean = false,\n    val installedPackageName: String? = null,\n    val latestVersion: String?,\n    val latestReleaseUrl: String?,\n    val starredAt: Long?,\n    val addedAt: Long,\n    val lastSyncedAt: Long,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/UpdateHistoryEntity.kt",
    "content": "package zed.rainxch.core.data.local.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport zed.rainxch.core.domain.model.InstallSource\n\n@Entity(tableName = \"update_history\")\ndata class UpdateHistoryEntity(\n    @PrimaryKey(autoGenerate = true)\n    val id: Long = 0,\n    val packageName: String,\n    val appName: String,\n    val repoOwner: String,\n    val repoName: String,\n    val fromVersion: String,\n    val toVersion: String,\n    val updatedAt: Long,\n    val updateSource: InstallSource,\n    val success: Boolean = true,\n    val errorMessage: String? = null,\n)\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/logging/KermitLogger.kt",
    "content": "package zed.rainxch.core.data.logging\n\nimport co.touchlab.kermit.Logger\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\n\nobject KermitLogger : GitHubStoreLogger {\n    override fun debug(message: String) {\n        Logger.d(message)\n    }\n\n    override fun info(message: String) {\n        Logger.i(message)\n    }\n\n    override fun warn(message: String) {\n        Logger.w(message)\n    }\n\n    override fun error(\n        message: String,\n        throwable: Throwable?,\n    ) {\n        Logger.e(message, throwable)\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.dto.AssetNetwork\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.GithubUser\n\nfun AssetNetwork.toDomain(): GithubAsset =\n    GithubAsset(\n        id = id,\n        name = name,\n        contentType = contentType,\n        size = size,\n        downloadUrl = downloadUrl,\n        uploader =\n            GithubUser(\n                id = uploader.id,\n                login = uploader.login,\n                avatarUrl = uploader.avatarUrl,\n                htmlUrl = uploader.htmlUrl,\n            ),\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/FavouriteRepoMappers.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.local.db.entities.FavoriteRepoEntity\nimport zed.rainxch.core.domain.model.FavoriteRepo\n\nfun FavoriteRepo.toEntity(): FavoriteRepoEntity =\n    FavoriteRepoEntity(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        isInstalled = isInstalled,\n        installedPackageName = installedPackageName,\n        latestVersion = latestVersion,\n        latestReleaseUrl = latestReleaseUrl,\n        addedAt = addedAt,\n        lastSyncedAt = lastSyncedAt,\n    )\n\nfun FavoriteRepoEntity.toDomain(): FavoriteRepo =\n    FavoriteRepo(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        isInstalled = isInstalled,\n        installedPackageName = installedPackageName,\n        latestVersion = latestVersion,\n        latestReleaseUrl = latestReleaseUrl,\n        addedAt = addedAt,\n        lastSyncedAt = lastSyncedAt,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/GithubAuthMappers.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.dto.GithubDeviceStartDto\nimport zed.rainxch.core.data.dto.GithubDeviceTokenErrorDto\nimport zed.rainxch.core.data.dto.GithubDeviceTokenSuccessDto\nimport zed.rainxch.core.domain.model.GithubDeviceStart\nimport zed.rainxch.core.domain.model.GithubDeviceTokenError\nimport zed.rainxch.core.domain.model.GithubDeviceTokenSuccess\n\nfun GithubDeviceStartDto.toDomain() =\n    GithubDeviceStart(\n        deviceCode = deviceCode,\n        userCode = userCode,\n        verificationUri = verificationUri,\n        verificationUriComplete = verificationUriComplete,\n        intervalSec = intervalSec,\n        expiresInSec = expiresInSec,\n    )\n\nfun GithubDeviceTokenSuccessDto.toDomain() =\n    GithubDeviceTokenSuccess(\n        accessToken = accessToken,\n        tokenType = tokenType,\n        expiresIn = expiresIn,\n        scope = scope,\n        refreshToken = refreshToken,\n        refreshTokenExpiresIn = refreshTokenExpiresIn,\n    )\n\nfun GithubDeviceTokenErrorDto.toDomain() =\n    GithubDeviceTokenError(\n        error = error,\n        errorDescription = errorDescription,\n    )\n\nfun GithubDeviceStart.toData() =\n    GithubDeviceStartDto(\n        deviceCode = deviceCode,\n        userCode = userCode,\n        verificationUri = verificationUri,\n        verificationUriComplete = verificationUriComplete,\n        intervalSec = intervalSec,\n        expiresInSec = expiresInSec,\n    )\n\nfun GithubDeviceTokenSuccess.toData() =\n    GithubDeviceTokenSuccessDto(\n        accessToken = accessToken,\n        tokenType = tokenType,\n        expiresIn = expiresIn,\n        scope = scope,\n        refreshToken = refreshToken,\n        refreshTokenExpiresIn = refreshTokenExpiresIn,\n    )\n\nfun GithubDeviceTokenError.toData() =\n    GithubDeviceTokenErrorDto(\n        error = error,\n        errorDescription = errorDescription,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/GithubRepoMapper.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.dto.GithubRepoNetworkModel\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.GithubUser\n\nfun GithubRepoNetworkModel.toSummary(): GithubRepoSummary =\n    GithubRepoSummary(\n        id = id,\n        name = name,\n        fullName = fullName,\n        owner =\n            GithubUser(\n                id = owner.id,\n                login = owner.login,\n                avatarUrl = owner.avatarUrl,\n                htmlUrl = owner.htmlUrl,\n            ),\n        description = description,\n        htmlUrl = htmlUrl,\n        stargazersCount = stargazersCount,\n        forksCount = forksCount,\n        language = language,\n        topics = topics,\n        releasesUrl = releasesUrl,\n        updatedAt = updatedAt,\n        defaultBranch = defaultBranch,\n        isFork = fork,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/InstalledAppsMappers.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.local.db.entities.InstalledAppEntity\nimport zed.rainxch.core.domain.model.InstalledApp\n\nfun InstalledApp.toEntity(): InstalledAppEntity =\n    InstalledAppEntity(\n        packageName = packageName,\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        installedVersion = installedVersion,\n        installedAssetName = installedAssetName,\n        installedAssetUrl = installedAssetUrl,\n        latestVersion = latestVersion,\n        latestAssetName = latestAssetName,\n        latestAssetUrl = latestAssetUrl,\n        latestAssetSize = latestAssetSize,\n        appName = appName,\n        installSource = installSource,\n        installedAt = installedAt,\n        lastCheckedAt = lastCheckedAt,\n        lastUpdatedAt = lastUpdatedAt,\n        isUpdateAvailable = isUpdateAvailable,\n        updateCheckEnabled = updateCheckEnabled,\n        releaseNotes = releaseNotes,\n        systemArchitecture = systemArchitecture,\n        fileExtension = fileExtension,\n        isPendingInstall = isPendingInstall,\n        installedVersionName = installedVersionName,\n        installedVersionCode = installedVersionCode,\n        latestVersionName = latestVersionName,\n        latestVersionCode = latestVersionCode,\n        signingFingerprint = signingFingerprint,\n    )\n\nfun InstalledAppEntity.toDomain(): InstalledApp =\n    InstalledApp(\n        packageName = packageName,\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        installedVersion = installedVersion,\n        installedAssetName = installedAssetName,\n        installedAssetUrl = installedAssetUrl,\n        latestVersion = latestVersion,\n        latestAssetName = latestAssetName,\n        latestAssetUrl = latestAssetUrl,\n        latestAssetSize = latestAssetSize,\n        appName = appName,\n        installSource = installSource,\n        installedAt = installedAt,\n        lastCheckedAt = lastCheckedAt,\n        lastUpdatedAt = lastUpdatedAt,\n        isUpdateAvailable = isUpdateAvailable,\n        updateCheckEnabled = updateCheckEnabled,\n        releaseNotes = releaseNotes,\n        systemArchitecture = systemArchitecture,\n        fileExtension = fileExtension,\n        isPendingInstall = isPendingInstall,\n        installedVersionName = installedVersionName,\n        installedVersionCode = installedVersionCode,\n        latestVersionName = latestVersionName,\n        latestVersionCode = latestVersionCode,\n        signingFingerprint = signingFingerprint,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/ReleaseNetwork.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.dto.ReleaseNetwork\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.GithubUser\n\nfun ReleaseNetwork.toDomain(): GithubRelease =\n    GithubRelease(\n        id = id,\n        tagName = tagName,\n        name = name,\n        author =\n            GithubUser(\n                id = author.id,\n                login = author.login,\n                avatarUrl = author.avatarUrl,\n                htmlUrl = author.htmlUrl,\n            ),\n        publishedAt = publishedAt ?: createdAt ?: \"\",\n        description = body,\n        assets = assets.map { it.toDomain() },\n        tarballUrl = tarballUrl,\n        zipballUrl = zipballUrl,\n        htmlUrl = htmlUrl,\n        isPrerelease = prerelease == true,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/StarredRepoMapper.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.local.db.entities.StarredRepositoryEntity\nimport zed.rainxch.core.domain.model.StarredRepository\n\nfun StarredRepository.toEntity(): StarredRepositoryEntity =\n    StarredRepositoryEntity(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        stargazersCount = stargazersCount,\n        forksCount = forksCount,\n        openIssuesCount = openIssuesCount,\n        isInstalled = isInstalled,\n        installedPackageName = installedPackageName,\n        latestVersion = latestVersion,\n        latestReleaseUrl = latestReleaseUrl,\n        starredAt = starredAt,\n        addedAt = addedAt,\n        lastSyncedAt = lastSyncedAt,\n    )\n\nfun StarredRepositoryEntity.toDomain(): StarredRepository =\n    StarredRepository(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        stargazersCount = stargazersCount,\n        forksCount = forksCount,\n        openIssuesCount = openIssuesCount,\n        isInstalled = isInstalled,\n        installedPackageName = installedPackageName,\n        latestVersion = latestVersion,\n        latestReleaseUrl = latestReleaseUrl,\n        starredAt = starredAt,\n        addedAt = addedAt,\n        lastSyncedAt = lastSyncedAt,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/UpdateHistoryMapper.kt",
    "content": "package zed.rainxch.core.data.mappers\n\nimport zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity\nimport zed.rainxch.core.domain.model.UpdateHistory\n\nfun UpdateHistory.toEntity(): UpdateHistoryEntity =\n    UpdateHistoryEntity(\n        id = id,\n        packageName = packageName,\n        appName = appName,\n        repoOwner = repoOwner,\n        repoName = repoName,\n        fromVersion = fromVersion,\n        toVersion = toVersion,\n        updatedAt = updatedAt,\n        updateSource = updateSource,\n        success = success,\n        errorMessage = errorMessage,\n    )\n\nfun UpdateHistoryEntity.toDomain(): UpdateHistory =\n    UpdateHistory(\n        id = id,\n        packageName = packageName,\n        appName = appName,\n        repoOwner = repoOwner,\n        repoName = repoName,\n        fromVersion = fromVersion,\n        toVersion = toVersion,\n        updatedAt = updatedAt,\n        updateSource = updateSource,\n        success = success,\n        errorMessage = errorMessage,\n    )\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt",
    "content": "package zed.rainxch.core.data.network\n\nimport io.ktor.client.HttpClient\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.repository.AuthenticationState\nimport zed.rainxch.core.domain.repository.RateLimitRepository\n\nclass GitHubClientProvider(\n    private val tokenStore: TokenStore,\n    private val rateLimitRepository: RateLimitRepository,\n    private val authenticationState: AuthenticationState,\n    proxyConfigFlow: StateFlow<ProxyConfig>,\n) {\n    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)\n    private val mutex = Mutex()\n\n    @Volatile\n    private var currentClient: HttpClient =\n        createGitHubHttpClient(\n            tokenStore = tokenStore,\n            rateLimitRepository = rateLimitRepository,\n            authenticationState = authenticationState,\n            scope = scope,\n            proxyConfig = proxyConfigFlow.value,\n        )\n\n    init {\n        proxyConfigFlow\n            .drop(1)\n            .distinctUntilChanged()\n            .onEach { proxyConfig ->\n                mutex.withLock {\n                    currentClient.close()\n                    currentClient =\n                        createGitHubHttpClient(\n                            tokenStore = tokenStore,\n                            rateLimitRepository = rateLimitRepository,\n                            authenticationState = authenticationState,\n                            scope = scope,\n                            proxyConfig = proxyConfig,\n                        )\n                }\n            }.launchIn(scope)\n    }\n\n    val client: HttpClient get() = currentClient\n\n    fun close() {\n        currentClient.close()\n        scope.cancel()\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.kt",
    "content": "package zed.rainxch.core.data.network\n\nimport io.ktor.client.*\nimport io.ktor.client.call.body\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.client.request.header\nimport io.ktor.client.statement.HttpResponse\nimport io.ktor.http.*\nimport io.ktor.serialization.kotlinx.json.*\nimport io.ktor.util.network.UnresolvedAddressException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.json.Json\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.data.network.interceptor.RateLimitInterceptor\nimport zed.rainxch.core.data.network.interceptor.UnauthorizedInterceptor\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.repository.AuthenticationState\nimport zed.rainxch.core.domain.repository.RateLimitRepository\nimport java.io.IOException\nimport kotlin.coroutines.cancellation.CancellationException\n\nexpect fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient\n\nfun createGitHubHttpClient(\n    tokenStore: TokenStore,\n    rateLimitRepository: RateLimitRepository,\n    authenticationState: AuthenticationState? = null,\n    scope: CoroutineScope? = null,\n    proxyConfig: ProxyConfig = ProxyConfig.None,\n): HttpClient {\n    val json =\n        Json {\n            ignoreUnknownKeys = true\n            isLenient = true\n        }\n\n    return createPlatformHttpClient(proxyConfig).config {\n        install(RateLimitInterceptor) {\n            this.rateLimitRepository = rateLimitRepository\n        }\n\n        if (authenticationState != null && scope != null) {\n            install(UnauthorizedInterceptor) {\n                this.authenticationState = authenticationState\n                this.scope = scope\n            }\n        }\n\n        install(ContentNegotiation) {\n            json(json)\n        }\n\n        install(HttpTimeout) {\n            requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS\n            connectTimeoutMillis = 30_000\n            socketTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS\n        }\n\n        install(HttpRequestRetry) {\n            maxRetries = 3\n            retryIf { _, response ->\n                val code = response.status.value\n                if (code == 403) {\n                    val remaining = response.headers[\"X-RateLimit-Remaining\"]?.toIntOrNull()\n                    if (remaining == 0) return@retryIf false\n                }\n                code in 500..<600\n            }\n            retryOnExceptionIf { _, cause ->\n                cause is HttpRequestTimeoutException ||\n                    cause is UnresolvedAddressException ||\n                    cause is IOException\n            }\n            exponentialDelay()\n        }\n\n        expectSuccess = false\n\n        defaultRequest {\n            url(\"https://api.github.com\")\n            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n            header(\"X-GitHub-Api-Version\", \"2022-11-28\")\n            header(HttpHeaders.UserAgent, \"GithubStore/1.0 (KMP)\")\n\n            val token =\n                tokenStore\n                    .blockingCurrentToken()\n                    ?.accessToken\n                    ?.trim()\n                    .orEmpty()\n            if (token.isNotEmpty()) {\n                header(HttpHeaders.Authorization, \"Bearer $token\")\n            }\n        }\n    }\n}\n\nsuspend inline fun <reified T> HttpClient.executeRequest(crossinline block: suspend HttpClient.() -> HttpResponse): Result<T> =\n    try {\n        val response = block()\n\n        if (response.status.isSuccess()) {\n            Result.success(response.body<T>())\n        } else {\n            Result.failure(\n                Exception(\"HTTP ${response.status.value}: ${response.status.description}\"),\n            )\n        }\n    } catch (e: RateLimitException) {\n        throw e\n    } catch (e: CancellationException) {\n        throw e\n    } catch (e: Exception) {\n        Result.failure(e)\n    }\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt",
    "content": "package zed.rainxch.core.data.network\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport zed.rainxch.core.domain.model.ProxyConfig\n\nobject ProxyManager {\n    private val _proxyConfig = MutableStateFlow<ProxyConfig>(ProxyConfig.None)\n    val currentProxyConfig: StateFlow<ProxyConfig> = _proxyConfig.asStateFlow()\n\n    fun setNoProxy() {\n        _proxyConfig.value = ProxyConfig.None\n    }\n\n    fun setSystemProxy() {\n        _proxyConfig.value = ProxyConfig.System\n    }\n\n    fun setHttpProxy(\n        host: String,\n        port: Int,\n        username: String? = null,\n        password: String? = null,\n    ) {\n        _proxyConfig.value = ProxyConfig.Http(host, port, username, password)\n    }\n\n    fun setSocksProxy(\n        host: String,\n        port: Int,\n        username: String? = null,\n        password: String? = null,\n    ) {\n        _proxyConfig.value = ProxyConfig.Socks(host, port, username, password)\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/RateLimitInterceptor.kt",
    "content": "package zed.rainxch.core.data.network.interceptor\n\nimport co.touchlab.kermit.Logger\nimport io.ktor.client.HttpClient\nimport io.ktor.client.plugins.HttpClientPlugin\nimport io.ktor.client.statement.HttpReceivePipeline\nimport io.ktor.client.statement.HttpResponse\nimport io.ktor.client.statement.HttpResponsePipeline\nimport io.ktor.http.Headers\nimport io.ktor.util.AttributeKey\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.model.RateLimitInfo\nimport zed.rainxch.core.domain.repository.RateLimitRepository\n\nclass RateLimitInterceptor(\n    private val rateLimitRepository: RateLimitRepository,\n) {\n    class Config {\n        var rateLimitRepository: RateLimitRepository? = null\n    }\n\n    companion object Plugin : HttpClientPlugin<Config, RateLimitInterceptor> {\n        override val key: AttributeKey<RateLimitInterceptor> =\n            AttributeKey(\"RateLimitInterceptor\")\n\n        override fun prepare(block: Config.() -> Unit): RateLimitInterceptor {\n            val config = Config().apply(block)\n            return RateLimitInterceptor(\n                rateLimitRepository =\n                    requireNotNull(config.rateLimitRepository) {\n                        \"RateLimitRepository must be provided\"\n                    },\n            )\n        }\n\n        override fun install(\n            plugin: RateLimitInterceptor,\n            scope: HttpClient,\n        ) {\n            scope.receivePipeline.intercept(HttpReceivePipeline.State) {\n                val response = subject\n\n                parseRateLimitFromHeaders(response.headers)?.let { rateLimitInfo ->\n                    plugin.rateLimitRepository.updateRateLimit(rateLimitInfo)\n\n                    if (response.status.value == 403 && rateLimitInfo.isExhausted) {\n                        throw RateLimitException(rateLimitInfo)\n                    }\n                }\n\n                proceedWith(subject)\n            }\n        }\n\n        private fun parseRateLimitFromHeaders(headers: Headers): RateLimitInfo? {\n            return try {\n                val limitHeader =\n                    headers[\"X-RateLimit-Limit\"]\n                        ?: return null.also { Logger.w { \"Missing X-RateLimit-Limit\" } }\n                val limit =\n                    limitHeader.toIntOrNull()\n                        ?: return null.also { Logger.w { \"Malformed X-RateLimit-Limit: $limitHeader\" } }\n                val remainingHeader =\n                    headers[\"X-RateLimit-Remaining\"]\n                        ?: return null.also { Logger.w { \"Missing X-RateLimit-Remaining\" } }\n                val remaining =\n                    remainingHeader.toIntOrNull()\n                        ?: return null.also { Logger.w { \"Malformed X-RateLimit-Remaining: $remainingHeader\" } }\n                val resetHeader =\n                    headers[\"X-RateLimit-Reset\"]\n                        ?: return null.also { Logger.w { \"Missing X-RateLimit-Reset\" } }\n                val reset =\n                    resetHeader.toLongOrNull()\n                        ?: return null.also { Logger.w { \"Malformed X-RateLimit-Reset: $resetHeader\" } }\n                val resource = headers[\"X-RateLimit-Resource\"] ?: \"core\"\n\n                RateLimitInfo(limit, remaining, reset, resource)\n            } catch (e: Exception) {\n                Logger.e(e) { \"Failed to parse rate limit headers\" }\n                null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/UnauthorizedInterceptor.kt",
    "content": "package zed.rainxch.core.data.network.interceptor\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.plugins.HttpClientPlugin\nimport io.ktor.client.statement.HttpReceivePipeline\nimport io.ktor.util.AttributeKey\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport zed.rainxch.core.domain.repository.AuthenticationState\n\nclass UnauthorizedInterceptor(\n    private val authenticationState: AuthenticationState,\n    private val scope: CoroutineScope,\n) {\n    class Config {\n        var authenticationState: AuthenticationState? = null\n        var scope: CoroutineScope? = null\n    }\n\n    companion object Plugin : HttpClientPlugin<Config, UnauthorizedInterceptor> {\n        override val key: AttributeKey<UnauthorizedInterceptor> =\n            AttributeKey(\"UnauthorizedInterceptor\")\n\n        override fun prepare(block: Config.() -> Unit): UnauthorizedInterceptor {\n            val config = Config().apply(block)\n            return UnauthorizedInterceptor(\n                authenticationState =\n                    requireNotNull(config.authenticationState) {\n                        \"AuthenticationState must be provided\"\n                    },\n                scope =\n                    requireNotNull(config.scope) {\n                        \"CoroutineScope must be provided\"\n                    },\n            )\n        }\n\n        override fun install(\n            plugin: UnauthorizedInterceptor,\n            scope: HttpClient,\n        ) {\n            scope.receivePipeline.intercept(HttpReceivePipeline.After) {\n                if (subject.status.value == 401) {\n                    plugin.scope.launch {\n                        plugin.authenticationState.notifySessionExpired()\n                    }\n                }\n                proceedWith(subject)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AuthenticationStateImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.domain.repository.AuthenticationState\n\nclass AuthenticationStateImpl(\n    private val tokenStore: TokenStore,\n) : AuthenticationState {\n    private val _sessionExpiredEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)\n    override val sessionExpiredEvent: SharedFlow<Unit> = _sessionExpiredEvent.asSharedFlow()\n\n    private val sessionExpiredMutex = Mutex()\n\n    override fun isUserLoggedIn(): Flow<Boolean> =\n        tokenStore\n            .tokenFlow()\n            .map {\n                it != null\n            }\n\n    override suspend fun isCurrentlyUserLoggedIn(): Boolean = tokenStore.currentToken() != null\n\n    override suspend fun notifySessionExpired() {\n        sessionExpiredMutex.withLock {\n            if (tokenStore.currentToken() == null) return@withLock\n            tokenStore.clear()\n            _sessionExpiredEvent.emit(Unit)\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/FavouritesRepositoryImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport zed.rainxch.core.data.local.db.dao.FavoriteRepoDao\nimport zed.rainxch.core.data.local.db.dao.InstalledAppDao\nimport zed.rainxch.core.data.mappers.toDomain\nimport zed.rainxch.core.data.mappers.toEntity\nimport zed.rainxch.core.domain.model.FavoriteRepo\nimport zed.rainxch.core.domain.repository.FavouritesRepository\n\nclass FavouritesRepositoryImpl(\n    private val favoriteRepoDao: FavoriteRepoDao,\n    private val installedAppsDao: InstalledAppDao,\n) : FavouritesRepository {\n    override fun getAllFavorites(): Flow<List<FavoriteRepo>> =\n        favoriteRepoDao\n            .getAllFavorites()\n            .map { favoriteRepos ->\n                favoriteRepos.map { favoriteRepo -> favoriteRepo.toDomain() }\n            }\n\n    override fun isFavorite(repoId: Long): Flow<Boolean> = favoriteRepoDao.isFavorite(repoId)\n\n    override suspend fun isFavoriteSync(repoId: Long): Boolean = favoriteRepoDao.isFavoriteSync(repoId)\n\n    suspend fun addFavorite(repo: FavoriteRepo) {\n        val installedApp = installedAppsDao.getAppByRepoId(repo.repoId)\n        favoriteRepoDao.insertFavorite(\n            repo\n                .toEntity()\n                .copy(\n                    isInstalled = installedApp != null,\n                    installedPackageName = installedApp?.packageName,\n                ),\n        )\n    }\n\n    override suspend fun toggleFavorite(repo: FavoriteRepo) {\n        if (favoriteRepoDao.isFavoriteSync(repo.repoId)) {\n            favoriteRepoDao.deleteFavoriteById(repo.repoId)\n        } else {\n            addFavorite(repo)\n        }\n    }\n\n    override suspend fun updateFavoriteInstallStatus(\n        repoId: Long,\n        installed: Boolean,\n        packageName: String?,\n    ) {\n        favoriteRepoDao.updateInstallStatus(repoId, installed, packageName)\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport androidx.room.immediateTransaction\nimport androidx.room.useWriterConnection\nimport co.touchlab.kermit.Logger\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.http.HttpHeaders\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport zed.rainxch.core.data.dto.ReleaseNetwork\nimport zed.rainxch.core.data.local.db.AppDatabase\nimport zed.rainxch.core.data.local.db.dao.InstalledAppDao\nimport zed.rainxch.core.data.local.db.dao.UpdateHistoryDao\nimport zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity\nimport zed.rainxch.core.data.mappers.toDomain\nimport zed.rainxch.core.data.mappers.toEntity\nimport zed.rainxch.core.data.network.executeRequest\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.InstallSource\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.Installer\n\nclass InstalledAppsRepositoryImpl(\n    private val database: AppDatabase,\n    private val installedAppsDao: InstalledAppDao,\n    private val historyDao: UpdateHistoryDao,\n    private val installer: Installer,\n    private val httpClient: HttpClient,\n    private val tweaksRepository: TweaksRepository,\n) : InstalledAppsRepository {\n    override suspend fun <R> executeInTransaction(block: suspend () -> R): R =\n        database.useWriterConnection { transactor ->\n            transactor.immediateTransaction {\n                block()\n            }\n        }\n\n    override fun getAllInstalledApps(): Flow<List<InstalledApp>> =\n        installedAppsDao\n            .getAllInstalledApps()\n            .map { it.map { app -> app.toDomain() } }\n\n    override fun getAppsWithUpdates(): Flow<List<InstalledApp>> =\n        installedAppsDao\n            .getAppsWithUpdates()\n            .map { it.map { app -> app.toDomain() } }\n\n    override fun getUpdateCount(): Flow<Int> = installedAppsDao.getUpdateCount()\n\n    override suspend fun getAppByPackage(packageName: String): InstalledApp? =\n        installedAppsDao\n            .getAppByPackage(packageName)\n            ?.toDomain()\n\n    override suspend fun getAppByRepoId(repoId: Long): InstalledApp? = installedAppsDao.getAppByRepoId(repoId)?.toDomain()\n\n    override fun getAppByRepoIdAsFlow(repoId: Long): Flow<InstalledApp?> =\n        installedAppsDao.getAppByRepoIdAsFlow(repoId).map { it?.toDomain() }\n\n    override suspend fun isAppInstalled(repoId: Long): Boolean = installedAppsDao.getAppByRepoId(repoId) != null\n\n    override suspend fun saveInstalledApp(app: InstalledApp) {\n        installedAppsDao.insertApp(app.toEntity())\n    }\n\n    override suspend fun deleteInstalledApp(packageName: String) {\n        installedAppsDao.deleteByPackageName(packageName)\n    }\n\n    private suspend fun fetchLatestPublishedRelease(\n        owner: String,\n        repo: String,\n    ): GithubRelease? {\n        return try {\n            val includePreReleases = tweaksRepository.getIncludePreReleases().first()\n\n            val releases =\n                httpClient\n                    .executeRequest<List<ReleaseNetwork>> {\n                        get(\"/repos/$owner/$repo/releases\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                            parameter(\"per_page\", 10)\n                        }\n                    }.getOrNull() ?: return null\n\n            val latest =\n                releases\n                    .asSequence()\n                    .filter { it.draft != true }\n                    .filter { includePreReleases || it.prerelease != true }\n                    .maxByOrNull { it.publishedAt ?: it.createdAt ?: \"\" }\n                    ?: return null\n\n            latest.toDomain()\n        } catch (e: Exception) {\n            Logger.e { \"Failed to fetch latest release for $owner/$repo: ${e.message}\" }\n            null\n        }\n    }\n\n    override suspend fun checkForUpdates(packageName: String): Boolean {\n        val app = installedAppsDao.getAppByPackage(packageName) ?: return false\n\n        try {\n            val latestRelease =\n                fetchLatestPublishedRelease(\n                    owner = app.repoOwner,\n                    repo = app.repoName,\n                )\n\n            if (latestRelease != null) {\n                val normalizedInstalledTag = normalizeVersion(app.installedVersion)\n                val normalizedLatestTag = normalizeVersion(latestRelease.tagName)\n\n                val installableAssets =\n                    latestRelease.assets.filter { asset ->\n                        installer.isAssetInstallable(asset.name)\n                    }\n                val primaryAsset = installer.choosePrimaryAsset(installableAssets)\n\n                // Only flag as update if the latest version is actually newer\n                // (not just different — avoids false \"downgrade\" notifications)\n                val isUpdateAvailable =\n                    if (normalizedInstalledTag == normalizedLatestTag) {\n                        false\n                    } else {\n                        isVersionNewer(normalizedLatestTag, normalizedInstalledTag)\n                    }\n\n                Logger.d {\n                    \"Update check for ${app.appName}: \" +\n                        \"installedTag=${app.installedVersion}, latestTag=${latestRelease.tagName}, \" +\n                        \"installedCode=${app.installedVersionCode}, \" +\n                        \"isUpdate=$isUpdateAvailable\"\n                }\n\n                installedAppsDao.updateVersionInfo(\n                    packageName = packageName,\n                    available = isUpdateAvailable,\n                    version = latestRelease.tagName,\n                    assetName = primaryAsset?.name,\n                    assetUrl = primaryAsset?.downloadUrl,\n                    assetSize = primaryAsset?.size,\n                    releaseNotes = latestRelease.description ?: \"\",\n                    timestamp = System.currentTimeMillis(),\n                    latestVersionName = latestRelease.tagName,\n                    latestVersionCode = null,\n                )\n\n                return isUpdateAvailable\n            } else {\n                installedAppsDao.updateLastChecked(packageName, System.currentTimeMillis())\n            }\n        } catch (e: Exception) {\n            Logger.e { \"Failed to check updates for $packageName: ${e.message}\" }\n            installedAppsDao.updateLastChecked(packageName, System.currentTimeMillis())\n        }\n\n        return false\n    }\n\n    override suspend fun checkAllForUpdates() {\n        val apps = installedAppsDao.getAllInstalledApps().first()\n        apps.forEach { app ->\n            if (app.updateCheckEnabled) {\n                try {\n                    checkForUpdates(app.packageName)\n                } catch (e: Exception) {\n                    Logger.w { \"Failed to check updates for ${app.packageName}: ${e.message}\" }\n                }\n            }\n        }\n    }\n\n    override suspend fun updateAppVersion(\n        packageName: String,\n        newTag: String,\n        newAssetName: String,\n        newAssetUrl: String,\n        newVersionName: String,\n        newVersionCode: Long,\n        signingFingerprint: String?,\n    ) {\n        val app = installedAppsDao.getAppByPackage(packageName) ?: return\n\n        Logger.d {\n            \"Updating app version: $packageName from ${app.installedVersion} to $newTag\"\n        }\n\n        historyDao.insertHistory(\n            UpdateHistoryEntity(\n                packageName = packageName,\n                appName = app.appName,\n                repoOwner = app.repoOwner,\n                repoName = app.repoName,\n                fromVersion = app.installedVersion,\n                toVersion = newTag,\n                updatedAt = System.currentTimeMillis(),\n                updateSource = InstallSource.THIS_APP,\n                success = true,\n            ),\n        )\n\n        installedAppsDao.updateApp(\n            app.copy(\n                installedVersion = newTag,\n                installedAssetName = newAssetName,\n                installedAssetUrl = newAssetUrl,\n                installedVersionName = newVersionName,\n                installedVersionCode = newVersionCode,\n                latestVersion = newTag,\n                latestAssetName = newAssetName,\n                latestAssetUrl = newAssetUrl,\n                latestVersionName = newVersionName,\n                latestVersionCode = newVersionCode,\n                isUpdateAvailable = false,\n                lastUpdatedAt = System.currentTimeMillis(),\n                lastCheckedAt = System.currentTimeMillis(),\n                signingFingerprint = signingFingerprint,\n            ),\n        )\n    }\n\n    override suspend fun updateApp(app: InstalledApp) {\n        installedAppsDao.updateApp(app.toEntity())\n    }\n\n    override suspend fun updatePendingStatus(\n        packageName: String,\n        isPending: Boolean,\n    ) {\n        val app = installedAppsDao.getAppByPackage(packageName) ?: return\n        installedAppsDao.updateApp(app.copy(isPendingInstall = isPending))\n    }\n\n    private fun normalizeVersion(version: String): String = version.removePrefix(\"v\").removePrefix(\"V\").trim()\n\n    /**\n     * Compare two version strings and return true if [candidate] is newer than [current].\n     * Handles semantic versioning (1.2.3), pre-release suffixes (1.2.3-beta.1),\n     * and falls back to lexicographic comparison for non-standard formats.\n     *\n     * Pre-release versions are considered older than their stable counterparts:\n     *   1.2.3-beta < 1.2.3  (per semver spec)\n     *\n     * This prevents false \"downgrade\" notifications when a user has a pre-release\n     * installed and the latest stable version has a lower or equal base version.\n     */\n    private fun isVersionNewer(\n        candidate: String,\n        current: String,\n    ): Boolean {\n        val candidateParsed = parseSemanticVersion(candidate)\n        val currentParsed = parseSemanticVersion(current)\n\n        if (candidateParsed != null && currentParsed != null) {\n            // Compare major.minor.patch\n            for (i in 0 until maxOf(candidateParsed.numbers.size, currentParsed.numbers.size)) {\n                val c = candidateParsed.numbers.getOrElse(i) { 0 }\n                val r = currentParsed.numbers.getOrElse(i) { 0 }\n                if (c > r) return true\n                if (c < r) return false\n            }\n            // Numbers are equal; compare pre-release suffixes\n            // No pre-release > has pre-release (e.g., 1.0.0 > 1.0.0-beta)\n            return when {\n                candidateParsed.preRelease == null && currentParsed.preRelease != null -> {\n                    true\n                }\n\n                candidateParsed.preRelease != null && currentParsed.preRelease == null -> {\n                    false\n                }\n\n                candidateParsed.preRelease != null && currentParsed.preRelease != null -> {\n                    comparePreRelease(candidateParsed.preRelease, currentParsed.preRelease) > 0\n                }\n\n                else -> {\n                    false\n                } // both null, versions are equal\n            }\n        }\n\n        // Fallback: lexicographic comparison (better than just \"not equal\")\n        return candidate > current\n    }\n\n    private data class SemanticVersion(\n        val numbers: List<Int>,\n        val preRelease: String?,\n    )\n\n    private fun parseSemanticVersion(version: String): SemanticVersion? {\n        // Split off pre-release suffix: \"1.2.3-beta.1\" -> \"1.2.3\" and \"beta.1\"\n        val hyphenIndex = version.indexOf('-')\n        val numberPart = if (hyphenIndex >= 0) version.substring(0, hyphenIndex) else version\n        val preRelease = if (hyphenIndex >= 0) version.substring(hyphenIndex + 1) else null\n\n        val parts = numberPart.split(\".\")\n        val numbers = parts.mapNotNull { it.toIntOrNull() }\n\n        // Only valid if we could parse at least one number and all parts were valid numbers\n        if (numbers.isEmpty() || numbers.size != parts.size) return null\n\n        return SemanticVersion(numbers, preRelease)\n    }\n\n    /**\n     * Compare pre-release identifiers per semver spec:\n     * Identifiers consisting of only digits are compared numerically.\n     * Identifiers with letters are compared lexically.\n     * Numeric identifiers always have lower precedence than alphanumeric.\n     * A larger set of pre-release fields has higher precedence if all preceding are equal.\n     */\n    private fun comparePreRelease(\n        a: String,\n        b: String,\n    ): Int {\n        val aParts = a.split(\".\")\n        val bParts = b.split(\".\")\n\n        for (i in 0 until minOf(aParts.size, bParts.size)) {\n            val aNum = aParts[i].toIntOrNull()\n            val bNum = bParts[i].toIntOrNull()\n\n            val cmp =\n                when {\n                    aNum != null && bNum != null -> aNum.compareTo(bNum)\n\n                    aNum != null -> -1\n\n                    // numeric < alphanumeric\n                    bNum != null -> 1\n\n                    else -> aParts[i].compareTo(bParts[i])\n                }\n            if (cmp != 0) return cmp\n        }\n\n        return aParts.size.compareTo(bParts.size)\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.edit\nimport androidx.datastore.preferences.core.intPreferencesKey\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport zed.rainxch.core.data.network.ProxyManager\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.repository.ProxyRepository\n\nclass ProxyRepositoryImpl(\n    private val preferences: DataStore<Preferences>,\n) : ProxyRepository {\n    private val proxyTypeKey = stringPreferencesKey(\"proxy_type\")\n    private val proxyHostKey = stringPreferencesKey(\"proxy_host\")\n    private val proxyPortKey = intPreferencesKey(\"proxy_port\")\n    private val proxyUsernameKey = stringPreferencesKey(\"proxy_username\")\n    private val proxyPasswordKey = stringPreferencesKey(\"proxy_password\")\n\n    override fun getProxyConfig(): Flow<ProxyConfig> =\n        preferences.data.map { prefs ->\n            when (prefs[proxyTypeKey]) {\n                \"system\" -> {\n                    ProxyConfig.System\n                }\n\n                \"http\" -> {\n                    val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }\n                    val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }\n                    if (host != null && port != null) {\n                        ProxyConfig.Http(\n                            host = host,\n                            port = port,\n                            username = prefs[proxyUsernameKey],\n                            password = prefs[proxyPasswordKey],\n                        )\n                    } else {\n                        ProxyConfig.None\n                    }\n                }\n\n                \"socks\" -> {\n                    val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }\n                    val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }\n                    if (host != null && port != null) {\n                        ProxyConfig.Socks(\n                            host = host,\n                            port = port,\n                            username = prefs[proxyUsernameKey],\n                            password = prefs[proxyPasswordKey],\n                        )\n                    } else {\n                        ProxyConfig.None\n                    }\n                }\n\n                else -> {\n                    ProxyConfig.None\n                }\n            }\n        }\n\n    override suspend fun setProxyConfig(config: ProxyConfig) {\n        // Persist first so config survives crashes, then apply in-memory\n        preferences.edit { prefs ->\n            when (config) {\n                is ProxyConfig.None -> {\n                    prefs[proxyTypeKey] = \"none\"\n                    prefs.remove(proxyHostKey)\n                    prefs.remove(proxyPortKey)\n                    prefs.remove(proxyUsernameKey)\n                    prefs.remove(proxyPasswordKey)\n                }\n\n                is ProxyConfig.System -> {\n                    prefs[proxyTypeKey] = \"system\"\n                    prefs.remove(proxyHostKey)\n                    prefs.remove(proxyPortKey)\n                    prefs.remove(proxyUsernameKey)\n                    prefs.remove(proxyPasswordKey)\n                }\n\n                is ProxyConfig.Http -> {\n                    prefs[proxyTypeKey] = \"http\"\n                    prefs[proxyHostKey] = config.host\n                    prefs[proxyPortKey] = config.port\n                    if (config.username != null) {\n                        prefs[proxyUsernameKey] = config.username!!\n                    } else {\n                        prefs.remove(proxyUsernameKey)\n                    }\n                    if (config.password != null) {\n                        prefs[proxyPasswordKey] = config.password!!\n                    } else {\n                        prefs.remove(proxyPasswordKey)\n                    }\n                }\n\n                is ProxyConfig.Socks -> {\n                    prefs[proxyTypeKey] = \"socks\"\n                    prefs[proxyHostKey] = config.host\n                    prefs[proxyPortKey] = config.port\n                    if (config.username != null) {\n                        prefs[proxyUsernameKey] = config.username!!\n                    } else {\n                        prefs.remove(proxyUsernameKey)\n                    }\n                    if (config.password != null) {\n                        prefs[proxyPasswordKey] = config.password!!\n                    } else {\n                        prefs.remove(proxyPasswordKey)\n                    }\n                }\n            }\n        }\n        applyToProxyManager(config)\n    }\n\n    private fun applyToProxyManager(config: ProxyConfig) {\n        when (config) {\n            is ProxyConfig.None -> {\n                ProxyManager.setNoProxy()\n            }\n\n            is ProxyConfig.System -> {\n                ProxyManager.setSystemProxy()\n            }\n\n            is ProxyConfig.Http -> {\n                ProxyManager.setHttpProxy(\n                    host = config.host,\n                    port = config.port,\n                    username = config.username,\n                    password = config.password,\n                )\n            }\n\n            is ProxyConfig.Socks -> {\n                ProxyManager.setSocksProxy(\n                    host = config.host,\n                    port = config.port,\n                    username = config.username,\n                    password = config.password,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/RateLimitRepositoryImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport zed.rainxch.core.domain.model.RateLimitInfo\nimport zed.rainxch.core.domain.repository.RateLimitRepository\nimport kotlin.time.Clock\n\nclass RateLimitRepositoryImpl : RateLimitRepository {\n    private val _rateLimitState = MutableStateFlow<RateLimitInfo?>(null)\n    override val rateLimitState: StateFlow<RateLimitInfo?> = _rateLimitState.asStateFlow()\n\n    private val _rateLimitExhaustedEvent = MutableSharedFlow<RateLimitInfo>(extraBufferCapacity = 1)\n    override val rateLimitExhaustedEvent: SharedFlow<RateLimitInfo> = _rateLimitExhaustedEvent\n\n    override fun updateRateLimit(rateLimitInfo: RateLimitInfo?) {\n        _rateLimitState.value = rateLimitInfo\n        if (rateLimitInfo?.isExhausted == true) {\n            _rateLimitExhaustedEvent.tryEmit(rateLimitInfo)\n        }\n    }\n\n    override fun getCurrentRateLimit(): RateLimitInfo? = _rateLimitState.value\n\n    override fun isCurrentlyLimited(): Boolean {\n        val info = getCurrentRateLimit() ?: return false\n        return info.isCurrentlyLimited()\n    }\n\n    override fun clear() {\n        _rateLimitState.value = null\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SeenReposRepositoryImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport zed.rainxch.core.data.local.db.dao.SeenRepoDao\nimport zed.rainxch.core.data.local.db.entities.SeenRepoEntity\nimport zed.rainxch.core.domain.repository.SeenReposRepository\n\nclass SeenReposRepositoryImpl(\n    private val seenRepoDao: SeenRepoDao,\n) : SeenReposRepository {\n    override fun getAllSeenRepoIds(): Flow<Set<Long>> =\n        seenRepoDao.getAllSeenRepoIds().map { it.toSet() }\n\n    override suspend fun markAsSeen(repoId: Long) {\n        seenRepoDao.insert(\n            SeenRepoEntity(\n                repoId = repoId,\n                seenAt = System.currentTimeMillis(),\n            ),\n        )\n    }\n\n    override suspend fun clearAll() {\n        seenRepoDao.clearAll()\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage zed.rainxch.core.data.repository\n\nimport co.touchlab.kermit.Logger\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.http.isSuccess\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withPermit\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport zed.rainxch.core.data.dto.GitHubStarredResponse\nimport zed.rainxch.core.data.local.db.dao.InstalledAppDao\nimport zed.rainxch.core.data.local.db.dao.StarredRepoDao\nimport zed.rainxch.core.data.mappers.toDomain\nimport zed.rainxch.core.data.mappers.toEntity\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.repository.StarredRepository\nimport kotlin.coroutines.cancellation.CancellationException\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\nclass StarredRepositoryImpl(\n    private val starredRepoDao: StarredRepoDao,\n    private val installedAppsDao: InstalledAppDao,\n    private val platform: Platform,\n    private val httpClient: HttpClient,\n) : StarredRepository {\n    companion object {\n        private const val SYNC_THRESHOLD_MS = 24 * 60 * 60 * 1000L // 24 hours\n    }\n\n    override fun getAllStarred(): Flow<List<zed.rainxch.core.domain.model.StarredRepository>> =\n        starredRepoDao\n            .getAllStarred()\n            .map { it.map { entity -> entity.toDomain() } }\n\n    override suspend fun isStarred(repoId: Long): Boolean = starredRepoDao.isStarred(repoId)\n\n    override suspend fun getLastSyncTime(): Long? = starredRepoDao.getLastSyncTime()\n\n    override suspend fun needsSync(): Boolean {\n        val lastSync = getLastSyncTime() ?: return true\n        val now = Clock.System.now().toEpochMilliseconds()\n        return (now - lastSync) > SYNC_THRESHOLD_MS\n    }\n\n    override suspend fun syncStarredRepos(forceRefresh: Boolean): Result<Unit> =\n        withContext(Dispatchers.IO) {\n            try {\n                if (!forceRefresh && !needsSync()) {\n                    return@withContext Result.success(Unit)\n                }\n\n                val allRepos = mutableListOf<GitHubStarredResponse>()\n                var page = 1\n                val perPage = 100\n\n                while (true) {\n                    val response =\n                        httpClient.get(\"/user/starred\") {\n                            parameter(\"per_page\", perPage)\n                            parameter(\"page\", page)\n                        }\n\n                    if (!response.status.isSuccess()) {\n                        if (response.status.value == 401) {\n                            return@withContext Result.failure(\n                                Exception(\"Authentication required. Please sign in with GitHub.\"),\n                            )\n                        }\n                        return@withContext Result.failure(\n                            Exception(\"Failed to fetch starred repos: ${response.status.description}\"),\n                        )\n                    }\n\n                    val repos: List<GitHubStarredResponse> = response.body()\n\n                    if (repos.isEmpty()) break\n\n                    allRepos.addAll(repos)\n\n                    if (repos.size < perPage) break\n                    page++\n                }\n\n                val now = Clock.System.now().toEpochMilliseconds()\n                val starredRepos = mutableListOf<zed.rainxch.core.domain.model.StarredRepository>()\n\n                coroutineScope {\n                    val semaphore = Semaphore(25)\n                    val deferredResults =\n                        allRepos.map { repo ->\n                            async {\n                                semaphore.withPermit {\n                                    val hasValidAssets =\n                                        checkForValidAssets(repo.owner.login, repo.name)\n                                    if (hasValidAssets) {\n                                        val installedApp = installedAppsDao.getAppByRepoId(repo.id)\n                                        zed.rainxch.core.domain.model.StarredRepository(\n                                            repoId = repo.id,\n                                            repoName = repo.name,\n                                            repoOwner = repo.owner.login,\n                                            repoOwnerAvatarUrl = repo.owner.avatarUrl,\n                                            repoDescription = repo.description,\n                                            primaryLanguage = repo.language,\n                                            repoUrl = repo.htmlUrl,\n                                            stargazersCount = repo.stargazersCount,\n                                            forksCount = repo.forksCount,\n                                            openIssuesCount = repo.openIssuesCount,\n                                            isInstalled = installedApp != null,\n                                            installedPackageName = installedApp?.packageName,\n                                            latestVersion = null,\n                                            latestReleaseUrl = null,\n                                            starredAt =\n                                                repo.starredAt?.let {\n                                                    Instant.parse(it).toEpochMilliseconds()\n                                                },\n                                            addedAt = now,\n                                            lastSyncedAt = now,\n                                        )\n                                    } else {\n                                        null\n                                    }\n                                }\n                            }\n                        }\n\n                    deferredResults.awaitAll().filterNotNull().let { validRepos ->\n                        starredRepos.addAll(validRepos)\n                    }\n                }\n\n                starredRepoDao.replaceAllStarred(starredRepos.map { it.toEntity() })\n\n                Result.success(Unit)\n            } catch (e: RateLimitException) {\n                throw e\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                Logger.e(e) { \"Failed to sync starred repos\" }\n                Result.failure(e)\n            }\n        }\n\n    private suspend fun checkForValidAssets(\n        owner: String,\n        repo: String,\n    ): Boolean {\n        return try {\n            val releasesResponse =\n                httpClient.get(\"/repos/$owner/$repo/releases\") {\n                    header(\"Accept\", \"application/vnd.github.v3+json\")\n                    parameter(\"per_page\", 10)\n                }\n\n            if (!releasesResponse.status.isSuccess()) {\n                return false\n            }\n\n            val allReleases: List<GithubReleaseNetworkModel> = releasesResponse.body()\n\n            val stableRelease =\n                allReleases.firstOrNull {\n                    it.draft != true && it.prerelease != true\n                } ?: return false\n\n            if (stableRelease.assets.isEmpty()) {\n                return false\n            }\n\n            val relevantAssets =\n                stableRelease.assets.filter { asset ->\n                    val name = asset.name.lowercase()\n                    when (platform) {\n                        Platform.ANDROID -> {\n                            name.endsWith(\".apk\")\n                        }\n\n                        Platform.WINDOWS -> {\n                            name.endsWith(\".msi\") || name.endsWith(\".exe\")\n                        }\n\n                        Platform.MACOS -> {\n                            name.endsWith(\".dmg\") || name.endsWith(\".pkg\")\n                        }\n\n                        Platform.LINUX -> {\n                            name.endsWith(\".appimage\") || name.endsWith(\".deb\") ||\n                                name.endsWith(\n                                    \".rpm\",\n                                )\n                        }\n                    }\n                }\n\n            relevantAssets.isNotEmpty()\n        } catch (e: RateLimitException) {\n            throw e\n        } catch (e: CancellationException) {\n            throw e\n        } catch (e: Exception) {\n            Logger.w(e) { \"Failed to check valid assets for $owner/$repo\" }\n            false\n        }\n    }\n\n    @Serializable\n    private data class GithubReleaseNetworkModel(\n        val assets: List<AssetNetworkModel>,\n        val draft: Boolean? = null,\n        val prerelease: Boolean? = null,\n        @SerialName(\"published_at\") val publishedAt: String? = null,\n    )\n\n    @Serializable\n    private data class AssetNetworkModel(\n        val name: String,\n    )\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt",
    "content": "package zed.rainxch.core.data.repository\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.booleanPreferencesKey\nimport androidx.datastore.preferences.core.edit\nimport androidx.datastore.preferences.core.longPreferencesKey\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.core.domain.repository.TweaksRepository\n\nclass TweaksRepositoryImpl(\n    private val preferences: DataStore<Preferences>,\n) : TweaksRepository {\n    private val THEME_KEY = stringPreferencesKey(\"app_theme\")\n    private val AMOLED_KEY = booleanPreferencesKey(\"amoled_theme\")\n    private val IS_DARK_THEME_KEY = booleanPreferencesKey(\"is_dark_theme\")\n    private val FONT_KEY = stringPreferencesKey(\"font_theme\")\n    private val AUTO_DETECT_CLIPBOARD_KEY = booleanPreferencesKey(\"auto_detect_clipboard_links\")\n    private val INSTALLER_TYPE_KEY = stringPreferencesKey(\"installer_type\")\n    private val AUTO_UPDATE_KEY = booleanPreferencesKey(\"auto_update_enabled\")\n    private val UPDATE_CHECK_INTERVAL_KEY = longPreferencesKey(\"update_check_interval_hours\")\n    private val INCLUDE_PRE_RELEASES_KEY = booleanPreferencesKey(\"include_pre_releases\")\n    private val LIQUID_GLASS_ENABLED_KEY = booleanPreferencesKey(\"liquid_glass_enabled\")\n    private val HIDE_SEEN_ENABLED_KEY = booleanPreferencesKey(\"hide_seen_enabled\")\n\n    override fun getThemeColor(): Flow<AppTheme> =\n        preferences.data.map { prefs ->\n            val themeName = prefs[THEME_KEY]\n            AppTheme.fromName(themeName)\n        }\n\n    override suspend fun setThemeColor(theme: AppTheme) {\n        preferences.edit { prefs ->\n            prefs[THEME_KEY] = theme.name\n        }\n    }\n\n    override fun getIsDarkTheme(): Flow<Boolean?> =\n        preferences.data.map { prefs ->\n            prefs[IS_DARK_THEME_KEY]\n        }\n\n    override suspend fun setDarkTheme(isDarkTheme: Boolean?) {\n        preferences.edit { prefs ->\n            if (isDarkTheme == null) {\n                prefs.remove(IS_DARK_THEME_KEY)\n            } else {\n                prefs[IS_DARK_THEME_KEY] = isDarkTheme\n            }\n        }\n    }\n\n    override fun getAmoledTheme(): Flow<Boolean> =\n        preferences.data.map { prefs ->\n            prefs[AMOLED_KEY] ?: false\n        }\n\n    override suspend fun setAmoledTheme(enabled: Boolean) {\n        preferences.edit { prefs ->\n            prefs[AMOLED_KEY] = enabled\n        }\n    }\n\n    override fun getFontTheme(): Flow<FontTheme> =\n        preferences.data.map { prefs ->\n            val fontName = prefs[FONT_KEY]\n            FontTheme.fromName(fontName)\n        }\n\n    override suspend fun setFontTheme(fontTheme: FontTheme) {\n        preferences.edit { prefs ->\n            prefs[FONT_KEY] = fontTheme.name\n        }\n    }\n\n    override fun getAutoDetectClipboardLinks(): Flow<Boolean> =\n        preferences.data.map { prefs ->\n            prefs[AUTO_DETECT_CLIPBOARD_KEY] ?: false\n        }\n\n    override suspend fun setAutoDetectClipboardLinks(enabled: Boolean) {\n        preferences.edit { prefs ->\n            prefs[AUTO_DETECT_CLIPBOARD_KEY] = enabled\n        }\n    }\n\n    override fun getInstallerType(): Flow<InstallerType> =\n        preferences.data.map { prefs ->\n            val name = prefs[INSTALLER_TYPE_KEY]\n            InstallerType.fromName(name)\n        }\n\n    override suspend fun setInstallerType(type: InstallerType) {\n        preferences.edit { prefs ->\n            prefs[INSTALLER_TYPE_KEY] = type.name\n        }\n    }\n\n    override fun getAutoUpdateEnabled(): Flow<Boolean> =\n        preferences.data.map { prefs ->\n            prefs[AUTO_UPDATE_KEY] ?: false\n        }\n\n    override suspend fun setAutoUpdateEnabled(enabled: Boolean) {\n        preferences.edit { prefs ->\n            prefs[AUTO_UPDATE_KEY] = enabled\n        }\n    }\n\n    override fun getUpdateCheckInterval(): Flow<Long> =\n        preferences.data.map { prefs ->\n            prefs[UPDATE_CHECK_INTERVAL_KEY] ?: DEFAULT_UPDATE_CHECK_INTERVAL_HOURS\n        }\n\n    override suspend fun setUpdateCheckInterval(hours: Long) {\n        preferences.edit { prefs ->\n            prefs[UPDATE_CHECK_INTERVAL_KEY] = hours\n        }\n    }\n\n    override fun getIncludePreReleases(): Flow<Boolean> =\n        preferences.data.map { prefs ->\n            prefs[INCLUDE_PRE_RELEASES_KEY] ?: false\n        }\n\n    override suspend fun setIncludePreReleases(enabled: Boolean) {\n        preferences.edit { prefs ->\n            prefs[INCLUDE_PRE_RELEASES_KEY] = enabled\n        }\n    }\n\n    override fun getLiquidGlassEnabled(): Flow<Boolean> =\n        preferences.data.map { prefs ->\n            prefs[LIQUID_GLASS_ENABLED_KEY] ?: true\n        }\n\n    override suspend fun setLiquidGlassEnabled(enabled: Boolean) {\n        preferences.edit { prefs ->\n            prefs[LIQUID_GLASS_ENABLED_KEY] = enabled\n        }\n    }\n\n    override fun getHideSeenEnabled(): Flow<Boolean> =\n        preferences.data.map { prefs ->\n            prefs[HIDE_SEEN_ENABLED_KEY] ?: false\n        }\n\n    override suspend fun setHideSeenEnabled(enabled: Boolean) {\n        preferences.edit { prefs ->\n            prefs[HIDE_SEEN_ENABLED_KEY] = enabled\n        }\n    }\n\n    companion object {\n        const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L\n    }\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/FileLocationsProvider.kt",
    "content": "package zed.rainxch.core.data.services\n\ninterface FileLocationsProvider {\n    fun appDownloadsDir(): String\n\n    fun userDownloadsDir(): String\n\n    fun setExecutableIfNeeded(path: String)\n\n    fun getCacheSizeBytes(): Long\n\n    fun clearCacheFiles(): Boolean\n}\n"
  },
  {
    "path": "core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/LocalizationManager.kt",
    "content": "package zed.rainxch.core.data.services\n\ninterface LocalizationManager {\n    /**\n     * Returns the current device language code in ISO 639-1 format (e.g., \"en\", \"zh\", \"ja\")\n     * Can include region code if available (e.g., \"zh-CN\", \"pt-BR\")\n     */\n    fun getCurrentLanguageCode(): String\n\n    /**\n     * Returns the primary language code without region (e.g., \"zh\" from \"zh-CN\")\n     */\n    fun getPrimaryLanguageCode(): String\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt",
    "content": "package zed.rainxch.core.data.di\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport org.koin.dsl.module\nimport zed.rainxch.core.data.local.data_store.createDataStore\nimport zed.rainxch.core.data.local.db.AppDatabase\nimport zed.rainxch.core.data.local.db.initDatabase\nimport zed.rainxch.core.data.services.DesktopInstallerInfoExtractor\nimport zed.rainxch.core.data.utils.DesktopAppLauncher\nimport zed.rainxch.core.data.utils.DesktopBrowserHelper\nimport zed.rainxch.core.data.utils.DesktopClipboardHelper\nimport zed.rainxch.core.data.services.DesktopDownloader\nimport zed.rainxch.core.data.services.DesktopFileLocationsProvider\nimport zed.rainxch.core.data.services.DesktopInstaller\nimport zed.rainxch.core.data.services.DesktopLocalizationManager\nimport zed.rainxch.core.data.services.DesktopPackageMonitor\nimport zed.rainxch.core.data.services.DesktopUpdateScheduleManager\nimport zed.rainxch.core.data.services.FileLocationsProvider\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.system.InstallerStatusProvider\nimport zed.rainxch.core.domain.system.UpdateScheduleManager\nimport zed.rainxch.core.data.services.LocalizationManager\nimport zed.rainxch.core.data.services.DesktopInstallerStatusProvider\nimport zed.rainxch.core.data.utils.DesktopShareManager\nimport zed.rainxch.core.domain.network.Downloader\nimport zed.rainxch.core.domain.system.PackageMonitor\nimport zed.rainxch.core.domain.utils.AppLauncher\nimport zed.rainxch.core.domain.utils.BrowserHelper\nimport zed.rainxch.core.domain.utils.ClipboardHelper\nimport zed.rainxch.core.domain.utils.ShareManager\n\nactual val corePlatformModule = module {\n    // Core\n    single<Downloader> {\n        DesktopDownloader(\n            files = get(),\n        )\n    }\n\n    single<Installer> {\n        DesktopInstaller(\n            platform = get(),\n            installerInfoExtractor = DesktopInstallerInfoExtractor(),\n        )\n    }\n\n    single<FileLocationsProvider> {\n        DesktopFileLocationsProvider(\n            platform = get()\n        )\n    }\n\n    single<PackageMonitor> {\n        DesktopPackageMonitor()\n    }\n\n    single<LocalizationManager> {\n        DesktopLocalizationManager()\n    }\n\n    // Locals\n\n    single<AppDatabase> {\n        initDatabase()\n    }\n\n    single<DataStore<Preferences>> {\n        createDataStore()\n    }\n\n\n    // Utils\n\n    single<BrowserHelper> {\n        DesktopBrowserHelper()\n    }\n\n    single<ClipboardHelper> {\n        DesktopClipboardHelper()\n    }\n\n    single<AppLauncher> {\n        DesktopAppLauncher(\n            logger = get(),\n            platform = get()\n        )\n    }\n\n    single<ShareManager> {\n        DesktopShareManager()\n    }\n\n    single<InstallerStatusProvider> {\n        DesktopInstallerStatusProvider()\n    }\n\n    single<UpdateScheduleManager> {\n        DesktopUpdateScheduleManager()\n    }\n}"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/local/data_store/createDataStore.kt",
    "content": "package zed.rainxch.core.data.local.data_store\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport java.io.File\n\nfun createDataStore(): DataStore<Preferences> =\n    createDataStore(\n        producePath = {\n            val file = File(System.getProperty(\"java.io.tmpdir\"), dataStoreFileName)\n            file.absolutePath\n        },\n    )\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt",
    "content": "package zed.rainxch.core.data.local.db\n\nimport androidx.room.Room\nimport androidx.sqlite.driver.bundled.BundledSQLiteDriver\nimport kotlinx.coroutines.Dispatchers\nimport java.io.File\n\nfun initDatabase(): AppDatabase {\n    val dbFile = File(System.getProperty(\"java.io.tmpdir\"), \"github_store.db\")\n    return Room\n        .databaseBuilder<AppDatabase>(\n            name = dbFile.absolutePath,\n        ).setDriver(BundledSQLiteDriver())\n        .setQueryCoroutineContext(Dispatchers.IO)\n        .fallbackToDestructiveMigration(true)\n        .build()\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/model/LinuxPackageType.kt",
    "content": "package zed.rainxch.core.data.model\n\nenum class LinuxPackageType {\n    DEB, // Debian/Ubuntu/Mint\n    RPM, // Fedora/RHEL/CentOS/openSUSE\n    UNIVERSAL, // Unknown - show AppImage only\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/model/LinuxTerminal.kt",
    "content": "package zed.rainxch.core.data.model\n\nenum class LinuxTerminal {\n    GNOME_TERMINAL,\n    KONSOLE,\n    XTERM,\n    XFCE4_TERMINAL,\n    ALACRITTY,\n    KITTY,\n    TILIX,\n    MATE_TERMINAL,\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt",
    "content": "package zed.rainxch.core.data.network\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.ProxyBuilder\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.http.Url\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport java.net.ProxySelector\nimport java.net.URI\n\nactual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient =\n    HttpClient(OkHttp) {\n        engine {\n            proxy =\n                when (proxyConfig) {\n                    is ProxyConfig.None -> {\n                        null\n                    }\n\n                    is ProxyConfig.System -> {\n                        val systemProxy =\n                            ProxySelector\n                                .getDefault()\n                                ?.select(URI(\"https://api.github.com\"))\n                                ?.firstOrNull { it.type() != java.net.Proxy.Type.DIRECT }\n\n                        if (systemProxy != null) {\n                            val addr = systemProxy.address() as? java.net.InetSocketAddress\n                            if (addr != null) {\n                                when (systemProxy.type()) {\n                                    java.net.Proxy.Type.HTTP -> {\n                                        ProxyBuilder.http(Url(\"http://${addr.hostString}:${addr.port}\"))\n                                    }\n\n                                    java.net.Proxy.Type.SOCKS -> {\n                                        ProxyBuilder.socks(addr.hostString, addr.port)\n                                    }\n\n                                    else -> {\n                                        null\n                                    }\n                                }\n                            } else {\n                                null\n                            }\n                        } else {\n                            null\n                        }\n                    }\n\n                    is ProxyConfig.Http -> {\n                        ProxyBuilder.http(Url(\"http://${proxyConfig.host}:${proxyConfig.port}\"))\n                    }\n\n                    is ProxyConfig.Socks -> {\n                        ProxyBuilder.socks(proxyConfig.host, proxyConfig.port)\n                    }\n                }\n        }\n    }\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.withContext\nimport okhttp3.Call\nimport okhttp3.Credentials\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport zed.rainxch.core.data.network.ProxyManager\nimport zed.rainxch.core.domain.model.DownloadProgress\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.network.Downloader\nimport java.io.File\nimport java.net.Authenticator\nimport java.net.InetSocketAddress\nimport java.net.PasswordAuthentication\nimport java.net.Proxy\nimport java.util.UUID\nimport java.util.concurrent.ConcurrentHashMap\n\nclass DesktopDownloader(\n    private val files: FileLocationsProvider,\n    private val proxyManager: ProxyManager = ProxyManager,\n) : Downloader {\n    private val activeDownloads = ConcurrentHashMap<String, Call>()\n    private val nameToId = ConcurrentHashMap<String, String>()\n\n    private fun buildClient(): OkHttpClient {\n        Authenticator.setDefault(null)\n\n        return OkHttpClient\n            .Builder()\n            .apply {\n                when (val config = proxyManager.currentProxyConfig.value) {\n                    is ProxyConfig.None -> {\n                        proxy(Proxy.NO_PROXY)\n                    }\n\n                    is ProxyConfig.System -> {}\n\n                    is ProxyConfig.Http -> {\n                        proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(config.host, config.port)))\n                        if (config.username != null && config.password != null) {\n                            proxyAuthenticator { _, response ->\n                                response.request\n                                    .newBuilder()\n                                    .header(\n                                        \"Proxy-Authorization\",\n                                        Credentials.basic(config.username!!, config.password!!),\n                                    ).build()\n                            }\n                        }\n                    }\n\n                    is ProxyConfig.Socks -> {\n                        proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(config.host, config.port)))\n                        if (config.username != null && config.password != null) {\n                            Authenticator.setDefault(\n                                object : Authenticator() {\n                                    override fun getPasswordAuthentication() =\n                                        PasswordAuthentication(\n                                            config.username,\n                                            config.password!!.toCharArray(),\n                                        )\n                                },\n                            )\n                        }\n                    }\n                }\n            }.build()\n    }\n\n    override fun download(\n        url: String,\n        suggestedFileName: String?,\n    ): Flow<DownloadProgress> =\n        flow {\n            val client = buildClient()\n\n            val dir = File(files.userDownloadsDir())\n            if (!dir.exists()) dir.mkdirs()\n\n            val rawName =\n                suggestedFileName?.takeIf { it.isNotBlank() }\n                    ?: url\n                        .substringAfterLast('/')\n                        .substringBefore('?')\n                        .substringBefore('#')\n                        .ifBlank { \"asset-${UUID.randomUUID()}\" }\n            val safeName = rawName.substringAfterLast('/').substringAfterLast('\\\\')\n            require(safeName.isNotBlank() && safeName != \".\" && safeName != \"..\") {\n                \"Invalid file name: $rawName\"\n            }\n\n            val downloadId = UUID.randomUUID().toString()\n            val previous = nameToId.putIfAbsent(safeName, downloadId)\n            if (previous != null) {\n                throw IllegalStateException(\"A download for '$safeName' is already in progress\")\n            }\n\n            val destination = File(dir, safeName)\n            if (destination.exists()) {\n                Logger.d { \"Deleting existing file before download: ${destination.absolutePath}\" }\n                destination.delete()\n            }\n\n            Logger.d { \"Starting download: $url\" }\n\n            val request = Request.Builder().url(url).build()\n            val call = client.newCall(request)\n            activeDownloads[downloadId] = call\n\n            try {\n                call.execute().use { response ->\n                    if (!response.isSuccessful) {\n                        throw kotlinx.io.IOException(\"Unexpected code ${response.code}\")\n                    }\n\n                    val body = response.body\n                    val contentLength = body.contentLength()\n                    val total = if (contentLength > 0) contentLength else null\n\n                    body.byteStream().use { input ->\n                        destination.outputStream().use { output ->\n                            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)\n                            var downloaded: Long = 0\n                            var bytesRead: Int\n                            while (input.read(buffer).also { bytesRead = it } != -1) {\n                                output.write(buffer, 0, bytesRead)\n                                downloaded += bytesRead\n                                val percent =\n                                    if (total != null) ((downloaded * 100L) / total).toInt() else null\n                                emit(DownloadProgress(downloaded, total, percent))\n                            }\n                        }\n                    }\n\n                    if (destination.exists() && destination.length() > 0) {\n                        Logger.d { \"Download complete: ${destination.absolutePath}\" }\n                        val finalDownloaded = destination.length()\n                        val finalPercent =\n                            if (total != null) ((finalDownloaded * 100L) / total).toInt() else 100\n                        emit(DownloadProgress(finalDownloaded, total, finalPercent))\n                    } else {\n                        throw IllegalStateException(\"File not ready after download: ${destination.absolutePath}\")\n                    }\n                }\n            } catch (e: Exception) {\n                destination.delete()\n                Logger.e(e) { \"Download failed\" }\n                throw e\n            } finally {\n                activeDownloads.remove(downloadId)\n                nameToId.remove(safeName)\n            }\n        }.flowOn(Dispatchers.IO)\n\n    override suspend fun saveToFile(\n        url: String,\n        suggestedFileName: String?,\n    ): String =\n        withContext(Dispatchers.IO) {\n            val rawName =\n                suggestedFileName?.takeIf { it.isNotBlank() }\n                    ?: url\n                        .substringAfterLast('/')\n                        .substringBefore('?')\n                        .substringBefore('#')\n                        .ifBlank { \"asset-${UUID.randomUUID()}\" }\n            val safeName = rawName.substringAfterLast('/').substringAfterLast('\\\\')\n            require(safeName.isNotBlank() && safeName != \".\" && safeName != \"..\") {\n                \"Invalid file name: $rawName\"\n            }\n\n            val file = File(files.userDownloadsDir(), safeName)\n            if (file.exists()) {\n                Logger.d { \"Deleting existing file before download: ${file.absolutePath}\" }\n                file.delete()\n            }\n\n            Logger.d { \"saveToFile downloading file...\" }\n            download(url, suggestedFileName).collect { }\n\n            file.absolutePath\n        }\n\n    override suspend fun getDownloadedFilePath(fileName: String): String? =\n        withContext(Dispatchers.IO) {\n            val file = File(files.userDownloadsDir(), fileName)\n            if (file.exists() && file.length() > 0) file.absolutePath else null\n        }\n\n    override suspend fun cancelDownload(fileName: String): Boolean =\n        withContext(Dispatchers.IO) {\n            var cancelled = false\n            var deleted = false\n\n            val downloadId = nameToId[fileName]\n            if (downloadId != null) {\n                activeDownloads[downloadId]?.let { call ->\n                    if (!call.isCanceled()) {\n                        call.cancel()\n                        cancelled = true\n                    }\n                }\n                activeDownloads.remove(downloadId)\n                nameToId.remove(fileName)\n            }\n\n            val file = File(files.userDownloadsDir(), fileName)\n            if (file.exists()) {\n                deleted = file.delete()\n            }\n\n            cancelled || deleted\n        }\n\n    companion object {\n        private const val DEFAULT_BUFFER_SIZE = 8 * 1024\n    }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopFileLocationsProvider.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport co.touchlab.kermit.Logger\nimport zed.rainxch.core.domain.model.Platform\nimport java.io.File\nimport java.nio.file.Files\nimport java.nio.file.attribute.PosixFilePermission\n\nclass DesktopFileLocationsProvider(\n    private val platform: Platform,\n) : FileLocationsProvider {\n    override fun appDownloadsDir(): String {\n        val baseDir =\n            when (platform) {\n                Platform.WINDOWS -> {\n                    val appData =\n                        System.getenv(\"LOCALAPPDATA\")\n                            ?: (System.getProperty(\"user.home\") + \"\\\\AppData\\\\Local\")\n                    File(appData, \"GithubStore\\\\Downloads\")\n                }\n\n                Platform.MACOS -> {\n                    val home = System.getProperty(\"user.home\")\n                    File(home, \"Library/Caches/GithubStore/Downloads\")\n                }\n\n                Platform.LINUX -> {\n                    val cacheHome =\n                        System.getenv(\"XDG_CACHE_HOME\")\n                            ?: (System.getProperty(\"user.home\") + \"/.cache\")\n                    File(cacheHome, \"githubstore/downloads\")\n                }\n\n                else -> {\n                    File(System.getProperty(\"user.home\"), \".githubstore/downloads\")\n                }\n            }\n\n        if (!baseDir.exists()) {\n            baseDir.mkdirs()\n        }\n\n        return baseDir.absolutePath\n    }\n\n    override fun setExecutableIfNeeded(path: String) {\n        if (platform == Platform.LINUX || platform == Platform.MACOS) {\n            try {\n                val file = File(path)\n                val filePath = file.toPath()\n\n                val perms = Files.getPosixFilePermissions(filePath).toMutableSet()\n\n                perms.add(PosixFilePermission.OWNER_EXECUTE)\n                perms.add(PosixFilePermission.GROUP_EXECUTE)\n                perms.add(PosixFilePermission.OTHERS_EXECUTE)\n\n                Files.setPosixFilePermissions(filePath, perms)\n            } catch (e: Exception) {\n                try {\n                    Runtime.getRuntime().exec(arrayOf(\"chmod\", \"+x\", path)).waitFor()\n                } catch (e2: Exception) {\n                    println(\"Warning: Could not set executable permission on $path\")\n                }\n            }\n        }\n    }\n\n    override fun userDownloadsDir(): String {\n        val appSubdirName = \"GitHub Store Downloads\"\n        val downloadsDir =\n            when (platform) {\n                Platform.WINDOWS -> {\n                    val userProfile =\n                        System.getenv(\"USERPROFILE\")\n                            ?: System.getProperty(\"user.home\")\n                    File(userProfile, \"Downloads\").resolve(appSubdirName)\n                }\n\n                Platform.MACOS -> {\n                    val home = System.getProperty(\"user.home\")\n                    File(home, \"Downloads\").resolve(appSubdirName)\n                }\n\n                Platform.LINUX -> {\n                    val xdgDownloads = getXdgDownloadsDir()\n                    val baseDir =\n                        if (xdgDownloads != null) {\n                            File(xdgDownloads)\n                        } else {\n                            val home = System.getProperty(\"user.home\")\n                            File(home, \"Downloads\")\n                        }\n                    baseDir.resolve(appSubdirName)\n                }\n\n                else -> {\n                    File(System.getProperty(\"user.home\"), \"Downloads\").resolve(appSubdirName)\n                }\n            }\n\n        if (!downloadsDir.exists()) {\n            downloadsDir.mkdirs()\n        }\n\n        return downloadsDir.absolutePath\n    }\n\n    override fun getCacheSizeBytes(): Long {\n        val appDir = File(appDownloadsDir())\n        val userDir = File(userDownloadsDir())\n        return calculateDirSize(appDir) + calculateDirSize(userDir)\n    }\n\n    override fun clearCacheFiles(): Boolean {\n        val appDir = File(appDownloadsDir())\n        val userDir = File(userDownloadsDir())\n        val appCleared = deleteDirectoryContents(appDir)\n        val userCleared = deleteDirectoryContents(userDir)\n        return appCleared && userCleared\n    }\n\n    private fun calculateDirSize(dir: File): Long {\n        if (!dir.exists()) return 0L\n        var size = 0L\n        dir.listFiles()?.forEach { file ->\n            size += if (file.isDirectory) calculateDirSize(file) else file.length()\n        }\n        return size\n    }\n\n    private fun deleteDirectoryContents(dir: File): Boolean {\n        if (!dir.exists()) return true\n        var allDeleted = true\n        dir.listFiles()?.forEach { file ->\n            if (file.isDirectory) {\n                if (!deleteDirectoryContents(file)) allDeleted = false\n                if (!file.delete()) allDeleted = false\n            } else {\n                if (!file.delete()) allDeleted = false\n            }\n        }\n        return allDeleted\n    }\n\n    private fun getXdgDownloadsDir(): String? {\n        return try {\n            val userDirsFile =\n                File(\n                    System.getProperty(\"user.home\"),\n                    \".config/user-dirs.dirs\",\n                )\n\n            if (userDirsFile.exists()) {\n                userDirsFile.readLines().forEach { line ->\n                    if (line.trim().startsWith(\"XDG_DOWNLOAD_DIR=\")) {\n                        val path =\n                            line\n                                .substringAfter(\"=\")\n                                .trim()\n                                .removeSurrounding(\"\\\"\")\n                                .replace(\"\\$HOME\", System.getProperty(\"user.home\"))\n                        return path\n                    }\n                }\n            }\n            null\n        } catch (e: Exception) {\n            Logger.w { \"Failed to read XDG user dirs: ${e.message}\" }\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport co.touchlab.kermit.Logger\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.data.model.LinuxPackageType\nimport zed.rainxch.core.data.model.LinuxTerminal\nimport zed.rainxch.core.domain.model.AssetArchitectureMatcher\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.SystemArchitecture\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.system.InstallerInfoExtractor\nimport java.awt.Desktop\nimport java.awt.Toolkit\nimport java.awt.datatransfer.StringSelection\nimport java.io.File\nimport java.io.IOException\nimport kotlin.collections.iterator\nimport kotlin.getValue\n\nclass DesktopInstaller(\n    private val platform: Platform,\n    private val installerInfoExtractor: InstallerInfoExtractor,\n) : Installer {\n    private val linuxPackageType: LinuxPackageType by lazy {\n        determineLinuxPackageType()\n    }\n\n    private val systemArchitecture: SystemArchitecture by lazy {\n        determineSystemArchitecture()\n    }\n\n    override fun getApkInfoExtractor(): InstallerInfoExtractor = installerInfoExtractor\n\n    override fun detectSystemArchitecture(): SystemArchitecture = systemArchitecture\n\n    override fun isObtainiumInstalled(): Boolean = false\n\n    override fun openInObtainium(\n        repoOwner: String,\n        repoName: String,\n        onOpenInstaller: () -> Unit,\n    ) {\n    }\n\n    override fun isAppManagerInstalled(): Boolean = false\n\n    override fun openInAppManager(\n        filePath: String,\n        onOpenInstaller: () -> Unit,\n    ) {\n    }\n\n    override fun openApp(packageName: String): Boolean {\n        // Desktop apps are launched differently per platform\n        Logger.d { \"Open app not supported on desktop for: $packageName\" }\n        return false\n    }\n\n    override fun openWithExternalInstaller(filePath: String) {\n        // Not applicable on desktop\n    }\n\n    override fun isAssetInstallable(assetName: String): Boolean {\n        val name = assetName.lowercase()\n\n        val hasValidExtension =\n            when (platform) {\n                Platform.ANDROID -> {\n                    name.endsWith(\".apk\")\n                }\n\n                Platform.WINDOWS -> {\n                    name.endsWith(\".msi\") || name.endsWith(\".exe\")\n                }\n\n                Platform.MACOS -> {\n                    name.endsWith(\".dmg\") || name.endsWith(\".pkg\")\n                }\n\n                Platform.LINUX -> {\n                    name.endsWith(\".appimage\") || name.endsWith(\".deb\") || name.endsWith(\".rpm\")\n                }\n            }\n\n        if (!hasValidExtension) return false\n\n        return isArchitectureCompatible(name, systemArchitecture)\n    }\n\n    override fun choosePrimaryAsset(assets: List<GithubAsset>): GithubAsset? {\n        if (assets.isEmpty()) return null\n\n        val priority =\n            when (platform) {\n                Platform.ANDROID -> {\n                    listOf(\".apk\")\n                }\n\n                Platform.WINDOWS -> {\n                    listOf(\".msi\", \".exe\")\n                }\n\n                Platform.MACOS -> {\n                    listOf(\".dmg\", \".pkg\")\n                }\n\n                Platform.LINUX -> {\n                    when (linuxPackageType) {\n                        LinuxPackageType.DEB -> listOf(\".appimage\", \".deb\", \".rpm\")\n                        LinuxPackageType.RPM -> listOf(\".appimage\", \".rpm\", \".deb\")\n                        LinuxPackageType.UNIVERSAL -> listOf(\".appimage\", \".deb\", \".rpm\")\n                    }\n                }\n            }\n\n        val compatibleAssets =\n            assets.filter { asset ->\n                isArchitectureCompatible(asset.name.lowercase(), systemArchitecture)\n            }\n\n        val assetsToConsider = compatibleAssets.ifEmpty { assets }\n\n        return assetsToConsider.maxByOrNull { asset ->\n            val name = asset.name.lowercase()\n\n            val extensionIdx = priority.indexOfFirst { name.endsWith(it) }\n            val extensionScore =\n                if (extensionIdx == -1) {\n                    -100000\n                } else {\n                    (priority.size - extensionIdx) * 10000\n                }\n\n            val archScore =\n                if (isExactArchitectureMatch(name, systemArchitecture)) {\n                    1000\n                } else {\n                    0\n                }\n\n            val sizeScore = (asset.size / 1000000).coerceAtMost(100)\n\n            extensionScore + archScore + sizeScore\n        }\n    }\n\n    private fun determineSystemArchitecture(): SystemArchitecture {\n        if (platform == Platform.MACOS) {\n            try {\n                val process = ProcessBuilder(\"uname\", \"-m\").start()\n                val output =\n                    process.inputStream\n                        .bufferedReader()\n                        .readText()\n                        .trim()\n                process.waitFor()\n\n                return when (output) {\n                    \"arm64\" -> SystemArchitecture.AARCH64\n                    \"x86_64\" -> SystemArchitecture.X86_64\n                    else -> SystemArchitecture.fromString(System.getProperty(\"os.arch\"))\n                }\n            } catch (_: Exception) {\n            }\n        }\n\n        val osArch = System.getProperty(\"os.arch\") ?: return SystemArchitecture.UNKNOWN\n        return SystemArchitecture.fromString(osArch)\n    }\n\n    private fun determineLinuxPackageType(): LinuxPackageType {\n        if (platform != Platform.LINUX) return LinuxPackageType.UNIVERSAL\n\n        return try {\n            val osRelease = tryReadOsRelease()\n            if (osRelease != null) {\n                val idLike = osRelease[\"ID_LIKE\"]?.lowercase() ?: \"\"\n                val id = osRelease[\"ID\"]?.lowercase() ?: \"\"\n\n                if (id in listOf(\"debian\", \"ubuntu\", \"linuxmint\", \"pop\", \"elementary\") ||\n                    idLike.contains(\"debian\") || idLike.contains(\"ubuntu\")\n                ) {\n                    Logger.d { \"Detected Debian-based distribution: $id\" }\n                    return LinuxPackageType.DEB\n                }\n\n                if (id in\n                    listOf(\n                        \"fedora\",\n                        \"rhel\",\n                        \"centos\",\n                        \"rocky\",\n                        \"almalinux\",\n                        \"opensuse\",\n                        \"suse\",\n                    ) ||\n                    idLike.contains(\"fedora\") || idLike.contains(\"rhel\") ||\n                    idLike.contains(\"suse\") || idLike.contains(\"centos\")\n                ) {\n                    Logger.d { \"Detected RPM-based distribution: $id\" }\n                    return LinuxPackageType.RPM\n                }\n            }\n\n            if (commandExists(\"apt\") || commandExists(\"apt-get\")) {\n                Logger.d { \"Detected package manager: apt\" }\n                return LinuxPackageType.DEB\n            }\n\n            if (commandExists(\"dnf\")) {\n                Logger.d { \"Detected package manager: dnf\" }\n                return LinuxPackageType.RPM\n            }\n\n            if (commandExists(\"yum\")) {\n                Logger.d { \"Detected package manager: yum\" }\n                return LinuxPackageType.RPM\n            }\n\n            if (commandExists(\"zypper\")) {\n                Logger.d { \"Detected package manager: zypper\" }\n                return LinuxPackageType.RPM\n            }\n\n            Logger.d { \"Could not determine package type, defaulting to UNIVERSAL\" }\n            LinuxPackageType.UNIVERSAL\n        } catch (e: Exception) {\n            Logger.w { \"Failed to detect Linux package type: ${e.message}\" }\n            LinuxPackageType.UNIVERSAL\n        }\n    }\n\n    private fun tryReadOsRelease(): Map<String, String>? {\n        val osReleaseFiles =\n            listOf(\n                \"/etc/os-release\",\n                \"/usr/lib/os-release\",\n            )\n\n        for (filePath in osReleaseFiles) {\n            try {\n                val file = File(filePath)\n                if (file.exists()) {\n                    val content = file.readText()\n                    return parseOsRelease(content)\n                }\n            } catch (e: Exception) {\n                Logger.w { \"Could not read $filePath: ${e.message}\" }\n            }\n        }\n        return null\n    }\n\n    private fun parseOsRelease(content: String): Map<String, String> {\n        val result = mutableMapOf<String, String>()\n        content.lines().forEach { line ->\n            val trimmed = line.trim()\n            if (trimmed.isNotEmpty() && !trimmed.startsWith(\"#\")) {\n                val parts = trimmed.split(\"=\", limit = 2)\n                if (parts.size == 2) {\n                    val key = parts[0].trim()\n                    val value = parts[1].trim().removeSurrounding(\"\\\"\")\n                    result[key] = value\n                }\n            }\n        }\n        return result\n    }\n\n    private fun commandExists(command: String): Boolean =\n        try {\n            val process = ProcessBuilder(\"which\", command).start()\n            process.waitFor() == 0\n        } catch (_: Exception) {\n            false\n        }\n\n    private fun isArchitectureCompatible(\n        assetName: String,\n        systemArch: SystemArchitecture,\n    ): Boolean {\n        val name = assetName.lowercase()\n\n        if (platform == Platform.MACOS) {\n            if (name.contains(\"universal\") || name.contains(\"darwin\")) {\n                return true\n            }\n\n            if ((name.endsWith(\".dmg\") || name.endsWith(\".pkg\")) &&\n                !listOf(\"x86_64\", \"amd64\", \"arm64\", \"aarch64\").any { name.contains(it) }\n            ) {\n                return true\n            }\n        }\n\n        return AssetArchitectureMatcher.isCompatible(name, systemArch)\n    }\n\n    private fun isExactArchitectureMatch(\n        assetName: String,\n        systemArch: SystemArchitecture,\n    ): Boolean = AssetArchitectureMatcher.isExactMatch(assetName, systemArch)\n\n    override suspend fun isSupported(extOrMime: String): Boolean {\n        val ext = extOrMime.lowercase().removePrefix(\".\")\n        return when (platform) {\n            Platform.WINDOWS -> ext in listOf(\"msi\", \"exe\")\n            Platform.MACOS -> ext in listOf(\"dmg\", \"pkg\")\n            Platform.LINUX -> ext in listOf(\"appimage\", \"deb\", \"rpm\")\n            else -> false\n        }\n    }\n\n    override suspend fun ensurePermissionsOrThrow(extOrMime: String) =\n        withContext(Dispatchers.IO) {\n            val ext = extOrMime.lowercase().removePrefix(\".\")\n\n            if (platform == Platform.LINUX && ext == \"appimage\") {\n                try {\n                    val tempFile = File.createTempFile(\"appimage_perm_test\", \".tmp\")\n                    try {\n                        val canSetExecutable = tempFile.setExecutable(true)\n                        if (!canSetExecutable) {\n                            throw IllegalStateException(\n                                \"Unable to set executable permissions. AppImage installation requires \" +\n                                    \"the ability to make files executable.\",\n                            )\n                        }\n                    } finally {\n                        tempFile.delete()\n                    }\n                } catch (e: IOException) {\n                    throw IllegalStateException(\n                        \"Failed to verify permission capabilities for AppImage installation: ${e.message}\",\n                        e,\n                    )\n                } catch (e: SecurityException) {\n                    throw IllegalStateException(\n                        \"Security restrictions prevent setting executable permissions for AppImage files.\",\n                        e,\n                    )\n                }\n            }\n        }\n\n    override fun uninstall(packageName: String) {\n        // Desktop doesn't have a unified uninstall mechanism\n        Logger.d { \"Uninstall not supported on desktop for: $packageName\" }\n    }\n\n    override suspend fun install(\n        filePath: String,\n        extOrMime: String,\n    ) = withContext(Dispatchers.IO) {\n        val file = File(filePath)\n        if (!file.exists()) {\n            throw IllegalStateException(\"File not found: $filePath\")\n        }\n\n        val ext = extOrMime.lowercase().removePrefix(\".\")\n\n        when (platform) {\n            Platform.WINDOWS -> installWindows(file, ext)\n            Platform.MACOS -> installMacOS(file, ext)\n            Platform.LINUX -> installLinux(file, ext)\n            else -> throw UnsupportedOperationException(\"Installation not supported on $platform\")\n        }\n    }\n\n    private fun installWindows(\n        file: File,\n        ext: String,\n    ) {\n        when (ext) {\n            \"msi\" -> {\n                val pb = ProcessBuilder(\"msiexec\", \"/i\", file.absolutePath)\n                pb.start()\n            }\n\n            \"exe\" -> {\n                if (Desktop.isDesktopSupported()) {\n                    Desktop.getDesktop().open(file)\n                } else {\n                    val pb = ProcessBuilder(file.absolutePath)\n                    pb.start()\n                }\n            }\n\n            else -> {\n                throw IllegalArgumentException(\"Unsupported Windows installer: .$ext\")\n            }\n        }\n    }\n\n    private fun installMacOS(\n        file: File,\n        ext: String,\n    ) {\n        when (ext) {\n            \"dmg\" -> {\n                val pb = ProcessBuilder(\"open\", file.absolutePath)\n                pb.start()\n\n                tryShowNotification(\n                    title = \"Installation Started\",\n                    message = \"Please drag the application to your Applications folder\",\n                )\n            }\n\n            \"pkg\" -> {\n                try {\n                    val script =\n                        \"\"\"\n                        do shell script \"installer -pkg '${file.absolutePath}' -target /\" with administrator privileges\n                        \"\"\".trimIndent()\n\n                    val pb = ProcessBuilder(\"osascript\", \"-e\", script)\n                    val process = pb.start()\n                    val exitCode = process.waitFor()\n\n                    if (exitCode != 0) {\n                        throw IOException(\"Installation cancelled or failed\")\n                    }\n\n                    Logger.d { \"PKG installed successfully\" }\n                } catch (e: Exception) {\n                    Logger.w { \"Automated install failed, opening installer GUI: ${e.message}\" }\n                    ProcessBuilder(\"open\", file.absolutePath).start()\n                }\n            }\n\n            else -> {\n                throw IllegalArgumentException(\"Unsupported macOS installer: .$ext\")\n            }\n        }\n    }\n\n    private fun tryShowNotification(\n        title: String,\n        message: String,\n    ) {\n        if (platform == Platform.MACOS) {\n            try {\n                val script =\n                    \"\"\"\n                    display notification \"$message\" with title \"$title\"\n                    \"\"\".trimIndent()\n                ProcessBuilder(\"osascript\", \"-e\", script).start()\n            } catch (e: Exception) {\n                Logger.w { \"Could not show macOS notification: ${e.message}\" }\n            }\n        } else {\n            try {\n                ProcessBuilder(\n                    \"notify-send\",\n                    title,\n                    message,\n                    \"-u\",\n                    \"critical\",\n                    \"-t\",\n                    \"10000\",\n                ).start()\n            } catch (e: Exception) {\n                Logger.w { \"Could not show notification: ${e.message}\" }\n            }\n        }\n    }\n\n    private fun installLinux(\n        file: File,\n        ext: String,\n    ) {\n        when (ext) {\n            \"appimage\" -> {\n                installAppImage(file)\n            }\n\n            \"deb\" -> {\n                installDebPackage(file)\n            }\n\n            \"rpm\" -> {\n                installRpmPackage(file)\n            }\n\n            else -> {\n                throw IllegalArgumentException(\"Unsupported Linux installer: .$ext\")\n            }\n        }\n    }\n\n    private fun installDebPackage(file: File) {\n        Logger.d { \"Installing DEB package: ${file.absolutePath}\" }\n\n        if (linuxPackageType == LinuxPackageType.RPM) {\n            Logger.i { \"Detected DEB package on RPM system. Initiating conversion flow.\" }\n            openTerminalForAlienConversion(file.absolutePath)\n            return\n        }\n\n        val installMethods =\n            listOf(\n                listOf(\"pkexec\", \"apt\", \"install\", \"-y\", file.absolutePath),\n                listOf(\"pkexec\", \"sh\", \"-c\", \"dpkg -i '${file.absolutePath}' || apt-get install -f -y\"),\n                listOf(\"gdebi-gtk\", file.absolutePath),\n                null,\n            )\n\n        for (method in installMethods) {\n            if (method == null) {\n                openTerminalForDebInstall(file.absolutePath)\n                return\n            }\n\n            try {\n                Logger.d { \"Trying installation method: ${method.joinToString(\" \")}\" }\n                val process = ProcessBuilder(method).start()\n                val exitCode = process.waitFor()\n\n                if (exitCode == 0) {\n                    Logger.d { \"DEB package installed successfully\" }\n                    tryShowNotification(\"Installation Complete\", \"Package installed successfully\")\n                    return\n                } else {\n                    Logger.w { \"Installation method failed with exit code: $exitCode\" }\n                }\n            } catch (e: IOException) {\n                Logger.w { \"Installation method not available: ${e.message}\" }\n            }\n        }\n\n        throw IOException(\"Could not install DEB package. Please install it manually.\")\n    }\n\n    private fun openTerminalForAlienConversion(filePath: String) {\n        Logger.d { \"Opening terminal for Alien conversion and installation\" }\n\n        val availableTerminals = detectAvailableTerminals()\n\n        if (availableTerminals.isEmpty()) {\n            Logger.e { \"No terminal emulator found for conversion\" }\n            tryShowNotification(\n                \"Conversion Required\",\n                \"Please install 'alien', convert '$filePath' to RPM, and install manually.\",\n            )\n            throw IOException(\"No terminal found to run Alien conversion.\")\n        }\n\n        val command =\n            buildString {\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'DEB Package on RPM System Detected'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo ''; \")\n                append(\"echo 'This package will be converted to RPM format.'; \")\n                append(\"echo 'This requires the \\\"alien\\\" tool.'; \")\n                append(\"echo ''; \")\n\n                append(\"if ! command -v alien &> /dev/null; then \")\n                append(\"echo 'Installing alien and rpm-build...'; \")\n                append(\"sudo dnf install -y alien rpm-build 2>/dev/null || \")\n                append(\"sudo yum install -y alien rpm-build 2>/dev/null || \")\n                append(\"sudo zypper install -y alien rpm-build 2>/dev/null; \")\n                append(\"fi; \")\n\n                append(\"if ! command -v alien &> /dev/null; then \")\n                append(\"echo ''; \")\n                append(\"echo 'ERROR: Failed to install alien.'; \")\n                append(\"echo 'Please install it manually: sudo dnf install alien rpm-build'; \")\n                append(\"echo ''; \")\n                append(\"echo 'Press Enter to close...'; read; exit 1; \")\n                append(\"fi; \")\n\n                append(\"echo ''; \")\n                append(\"echo 'Converting to RPM (this may take a minute)...'; \")\n                append(\"TMPDIR=/tmp/alien_install_$; \")\n                append($$\"mkdir -p \\\"$TMPDIR\\\" && cd \\\"$TMPDIR\\\" || exit 1; \")\n                append(\"cp '$filePath' ./package.deb; \")\n                append(\"sudo alien -r -c package.deb; \")\n\n                append(\"if [ ! -f *.rpm ]; then \")\n                append(\"echo ''; \")\n                append(\"echo 'ERROR: Conversion failed.'; \")\n                append($$\"cd .. && rm -rf \\\"$TMPDIR\\\"; \")\n                append(\"echo 'Press Enter to close...'; read; exit 1; \")\n                append(\"fi; \")\n\n                append(\"echo ''; \")\n                append(\"echo 'Installing converted RPM...'; \")\n                append(\"INSTALL_SUCCESS=0; \")\n\n                append(\"if sudo dnf install -y ./*.rpm 2>&1; then INSTALL_SUCCESS=1; \")\n                append(\"elif sudo yum install -y ./*.rpm 2>&1; then INSTALL_SUCCESS=1; \")\n                append(\"elif sudo zypper install -y --allow-unsigned-rpm ./*.rpm 2>&1; then INSTALL_SUCCESS=1; \")\n                append(\"elif sudo rpm -ivh --nodeps --force ./*.rpm 2>&1; then INSTALL_SUCCESS=1; \")\n                append(\"fi; \")\n\n                append(\"echo ''; \")\n                append($$\"if [ $INSTALL_SUCCESS -eq 1 ]; then \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'Installation Complete!'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"else \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'Installation Failed!'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo ''; \")\n                append(\"echo 'The RPM was created but installation failed.'; \")\n                append(\"echo 'This usually happens due to file conflicts.'; \")\n                append(\"echo ''; \")\n                append(\"echo 'The converted RPM is located at:'; \")\n                append($$\"echo \\\"$TMPDIR/\\\"*.rpm; \")\n                append(\"echo ''; \")\n                append(\"echo 'You can try installing it manually with:'; \")\n                append($$\"echo \\\"sudo rpm -ivh --force $TMPDIR/\\\"*.rpm; \")\n                append(\"echo ''; \")\n                append(\"echo 'Or open the file with your software manager.'; \")\n                append(\"fi; \")\n\n                append($$\"cd .. && rm -rf \\\"$TMPDIR\\\"; \")\n                append(\"echo ''; \")\n                append(\"echo 'Press Enter to close...'; read\")\n            }\n\n        runCommandInTerminal(command, availableTerminals)\n    }\n\n    private fun installRpmPackage(file: File) {\n        Logger.d { \"Installing RPM package: ${file.absolutePath}\" }\n\n        val installMethods =\n            listOf(\n                listOf(\"pkexec\", \"dnf\", \"install\", \"-y\", \"--nogpgcheck\", file.absolutePath),\n                listOf(\"pkexec\", \"yum\", \"install\", \"-y\", \"--nogpgcheck\", file.absolutePath),\n                listOf(\"pkexec\", \"zypper\", \"install\", \"-y\", \"--no-gpg-checks\", file.absolutePath),\n                listOf(\"pkexec\", \"rpm\", \"-ivh\", \"--nosignature\", file.absolutePath),\n                null,\n            )\n\n        for (method in installMethods) {\n            if (method == null) {\n                openTerminalForRpmInstall(file.absolutePath)\n                return\n            }\n\n            try {\n                Logger.d { \"Trying installation method: ${method.joinToString(\" \")}\" }\n                val process = ProcessBuilder(method).start()\n                val exitCode = process.waitFor()\n\n                if (exitCode == 0) {\n                    Logger.d { \"RPM package installed successfully\" }\n                    tryShowNotification(\"Installation Complete\", \"Package installed successfully\")\n                    return\n                } else {\n                    Logger.w { \"Installation method failed with exit code: $exitCode\" }\n                }\n            } catch (e: IOException) {\n                Logger.w { \"Installation method not available: ${e.message}\" }\n            }\n        }\n\n        throw IOException(\"Could not install RPM package. Please install it manually.\")\n    }\n\n    private fun openTerminalForDebInstall(filePath: String) {\n        val command =\n            buildString {\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'Installing DEB Package'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo ''; \")\n                append(\"sudo dpkg -i '$filePath' && sudo apt-get install -f -y; \")\n                append(\"echo ''; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'Installation Complete!'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo ''; \")\n                append(\"echo 'Press Enter to close...'; read\")\n            }\n\n        val availableTerminals = detectAvailableTerminals()\n        if (availableTerminals.isEmpty()) {\n            tryShowNotification(\n                \"Installation Required\",\n                \"Please install manually using your file manager\",\n            )\n            tryCopyToClipboard(\"sudo dpkg -i '$filePath' && sudo apt-get install -f -y\")\n            throw IOException(\"No terminal emulator found.\")\n        }\n\n        runCommandInTerminal(command, availableTerminals)\n    }\n\n    private fun openTerminalForRpmInstall(filePath: String) {\n        val command =\n            buildString {\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'Installing RPM Package'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo ''; \")\n                append(\"sudo dnf install -y --nogpgcheck '$filePath' 2>/dev/null || \")\n                append(\"sudo yum install -y --nogpgcheck '$filePath' 2>/dev/null || \")\n                append(\"sudo zypper install -y --no-gpg-checks '$filePath' 2>/dev/null || \")\n                append(\"sudo rpm -ivh --nosignature '$filePath'; \")\n                append(\"echo ''; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo 'Installation Complete!'; \")\n                append(\"echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; \")\n                append(\"echo ''; \")\n                append(\"echo 'Press Enter to close...'; read\")\n            }\n\n        val availableTerminals = detectAvailableTerminals()\n        if (availableTerminals.isEmpty()) {\n            tryShowNotification(\n                \"Installation Required\",\n                \"Please install manually using your file manager\",\n            )\n            tryCopyToClipboard(\"sudo dnf install -y --nogpgcheck '$filePath'\")\n            throw IOException(\"No terminal emulator found.\")\n        }\n\n        runCommandInTerminal(command, availableTerminals)\n    }\n\n    private fun runCommandInTerminal(\n        command: String,\n        terminals: List<LinuxTerminal>,\n    ) {\n        for (terminal in terminals) {\n            try {\n                Logger.d { \"Trying terminal: ${terminal.name}\" }\n                val processBuilder =\n                    when (terminal) {\n                        LinuxTerminal.GNOME_TERMINAL -> {\n                            ProcessBuilder(\n                                \"gnome-terminal\",\n                                \"--\",\n                                \"bash\",\n                                \"-c\",\n                                command,\n                            )\n                        }\n\n                        LinuxTerminal.KONSOLE -> {\n                            ProcessBuilder(\n                                \"konsole\",\n                                \"-e\",\n                                \"bash\",\n                                \"-c\",\n                                command,\n                            )\n                        }\n\n                        LinuxTerminal.XTERM -> {\n                            ProcessBuilder(\n                                \"xterm\",\n                                \"-e\",\n                                \"bash\",\n                                \"-c\",\n                                command,\n                            )\n                        }\n\n                        LinuxTerminal.XFCE4_TERMINAL -> {\n                            ProcessBuilder(\n                                \"xfce4-terminal\",\n                                \"-e\",\n                                \"bash -c \\\"$command\\\"\",\n                            )\n                        }\n\n                        LinuxTerminal.ALACRITTY -> {\n                            ProcessBuilder(\n                                \"alacritty\",\n                                \"-e\",\n                                \"bash\",\n                                \"-c\",\n                                command,\n                            )\n                        }\n\n                        LinuxTerminal.KITTY -> {\n                            ProcessBuilder(\n                                \"kitty\",\n                                \"bash\",\n                                \"-c\",\n                                command,\n                            )\n                        }\n\n                        LinuxTerminal.TILIX -> {\n                            ProcessBuilder(\n                                \"tilix\",\n                                \"-e\",\n                                \"bash -c \\\"$command\\\"\",\n                            )\n                        }\n\n                        LinuxTerminal.MATE_TERMINAL -> {\n                            ProcessBuilder(\n                                \"mate-terminal\",\n                                \"-e\",\n                                \"bash -c \\\"$command\\\"\",\n                            )\n                        }\n                    }\n\n                processBuilder.start()\n                Logger.d { \"Terminal opened successfully: ${terminal.name}\" }\n                return\n            } catch (e: IOException) {\n                Logger.w { \"Failed to open ${terminal.name}: ${e.message}\" }\n            }\n        }\n        throw IOException(\"Could not open any terminal emulator\")\n    }\n\n    private fun detectAvailableTerminals(): List<LinuxTerminal> {\n        val availableTerminals = mutableListOf<LinuxTerminal>()\n\n        val terminalCommands =\n            mapOf(\n                LinuxTerminal.GNOME_TERMINAL to \"gnome-terminal\",\n                LinuxTerminal.KONSOLE to \"konsole\",\n                LinuxTerminal.XFCE4_TERMINAL to \"xfce4-terminal\",\n                LinuxTerminal.ALACRITTY to \"alacritty\",\n                LinuxTerminal.KITTY to \"kitty\",\n                LinuxTerminal.TILIX to \"tilix\",\n                LinuxTerminal.MATE_TERMINAL to \"mate-terminal\",\n                LinuxTerminal.XTERM to \"xterm\",\n            )\n\n        for ((terminal, command) in terminalCommands) {\n            if (commandExists(command)) {\n                availableTerminals.add(terminal)\n                Logger.d { \"Found terminal: $command\" }\n            }\n        }\n\n        return availableTerminals\n    }\n\n    private fun tryCopyToClipboard(text: String) {\n        try {\n            val clipboard = Toolkit.getDefaultToolkit().systemClipboard\n            clipboard.setContents(StringSelection(text), null)\n            Logger.d { \"Command copied to clipboard\" }\n        } catch (e: Exception) {\n            Logger.w { \"Could not copy to clipboard: ${e.message}\" }\n        }\n    }\n\n    private fun installAppImage(file: File) {\n        Logger.d { \"Installing AppImage: ${file.absolutePath}\" }\n\n        try {\n            Logger.d { \"Moving AppImage to ~/Applications...\" }\n            val installedFile = moveToApplicationsDirectory(file)\n            Logger.d { \"Moved to: ${installedFile.absolutePath}\" }\n\n            Logger.d { \"Setting executable permissions...\" }\n            val executableSet = installedFile.setExecutable(true, false)\n            Logger.d { \"Set executable via Java: $executableSet\" }\n\n            if (!executableSet) {\n                Logger.w { \"Failed to set executable via Java, trying chmod...\" }\n                val chmodProcess = ProcessBuilder(\"chmod\", \"+x\", installedFile.absolutePath).start()\n                val chmodExitCode = chmodProcess.waitFor()\n                Logger.d { \"chmod exit code: $chmodExitCode\" }\n            }\n\n            if (!installedFile.canExecute()) {\n                throw IllegalStateException(\"Failed to make AppImage executable\")\n            }\n\n            Logger.d { \"AppImage is now executable\" }\n\n            Logger.d { \"Launching AppImage...\" }\n            val process =\n                ProcessBuilder(installedFile.absolutePath)\n                    .inheritIO()\n                    .start()\n\n            Logger.d { \"AppImage launched successfully (PID: ${process.pid()})\" }\n\n            showInstallationNotification(installedFile)\n\n            Logger.d { \"AppImage installation completed successfully\" }\n        } catch (e: IOException) {\n            Logger.e { \"Failed to install AppImage: ${e.message}\" }\n            e.printStackTrace()\n            throw IllegalStateException(\n                \"Failed to install AppImage: ${e.message}. \" +\n                    \"Please ensure you have write permissions to ~/Applications folder.\",\n                e,\n            )\n        } catch (e: SecurityException) {\n            Logger.e { \"Security exception: ${e.message}\" }\n            e.printStackTrace()\n            throw IllegalStateException(\n                \"Security restrictions prevent installing AppImage.\",\n                e,\n            )\n        } catch (e: Exception) {\n            Logger.e { \"Unexpected error: ${e.message}\" }\n            e.printStackTrace()\n            throw IllegalStateException(\"Failed to install AppImage: ${e.message}\", e)\n        }\n    }\n\n    /**\n     * Move AppImage to ~/Applications directory\n     * Creates the directory if it doesn't exist\n     */\n    private fun moveToApplicationsDirectory(file: File): File {\n        val homeDir = System.getProperty(\"user.home\")\n        val applicationsDir = File(homeDir, \"Applications\")\n\n        if (!applicationsDir.exists()) {\n            Logger.d { \"Creating ~/Applications directory...\" }\n            val created = applicationsDir.mkdirs()\n            Logger.d { \"Directory created: $created\" }\n        }\n\n        if (file.parent == applicationsDir.absolutePath) {\n            Logger.d { \"AppImage already in ~/Applications, no move needed\" }\n            return file\n        }\n\n        val destinationFile = File(applicationsDir, file.name)\n        val finalDestination =\n            if (destinationFile.exists()) {\n                Logger.d { \"File already exists in ~/Applications, generating unique name\" }\n                generateUniqueFileName(applicationsDir, file.name)\n            } else {\n                destinationFile\n            }\n\n        Logger.d { \"Moving from: ${file.absolutePath}\" }\n        Logger.d { \"Moving to: ${finalDestination.absolutePath}\" }\n\n        file.copyTo(finalDestination, overwrite = false)\n        Logger.d { \"Copy successful, file size: ${finalDestination.length()} bytes\" }\n\n        val deleted = file.delete()\n        Logger.d { \"Original file deleted: $deleted\" }\n\n        if (!finalDestination.exists()) {\n            throw IllegalStateException(\"File was moved but doesn't exist at destination\")\n        }\n\n        return finalDestination\n    }\n\n    private fun showInstallationNotification(file: File) {\n        try {\n            val message = \"AppImage installed and launched from ~/Applications\"\n\n            Logger.i { message }\n            Logger.i { \"Location: ${file.absolutePath}\" }\n\n            ProcessBuilder(\n                \"notify-send\",\n                \"-i\",\n                \"application-x-executable\",\n                \"AppImage Installed\",\n                \"Installed to ~/Applications\\n\\nYou can find it at:\\n${file.name}\",\n            ).start()\n        } catch (e: Exception) {\n            Logger.d { \"Could not show notification: ${e.message}\" }\n        }\n    }\n\n    private fun generateUniqueFileName(\n        directory: File,\n        originalName: String,\n    ): File {\n        val nameWithoutExtension = originalName.substringBeforeLast(\".\")\n        val extension = originalName.substringAfterLast(\".\", \"\")\n\n        var counter = 1\n        var candidateFile: File\n\n        do {\n            val newName =\n                if (extension.isNotEmpty()) {\n                    \"${nameWithoutExtension}_$counter.$extension\"\n                } else {\n                    \"${nameWithoutExtension}_$counter\"\n                }\n            candidateFile = File(directory, newName)\n            counter++\n        } while (candidateFile.exists() && counter < 1000)\n\n        if (candidateFile.exists()) {\n            throw IllegalStateException(\"Could not generate unique filename\")\n        }\n\n        return candidateFile\n    }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstallerInfoExtractor.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport zed.rainxch.core.domain.model.ApkPackageInfo\nimport zed.rainxch.core.domain.system.InstallerInfoExtractor\n\nclass DesktopInstallerInfoExtractor : InstallerInfoExtractor {\n    override suspend fun extractPackageInfo(filePath: String): ApkPackageInfo? = null\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstallerStatusProvider.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport zed.rainxch.core.domain.model.ShizukuAvailability\nimport zed.rainxch.core.domain.system.InstallerStatusProvider\n\n/**\n * Desktop (JVM) no-op implementation of [InstallerStatusProvider].\n * Shizuku is Android-only, so this always reports UNAVAILABLE.\n */\nclass DesktopInstallerStatusProvider : InstallerStatusProvider {\n    override val shizukuAvailability: StateFlow<ShizukuAvailability> =\n        MutableStateFlow(ShizukuAvailability.UNAVAILABLE).asStateFlow()\n\n    override fun requestShizukuPermission() {\n        // No-op on desktop\n    }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopLocalizationManager.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport java.util.Locale\n\nclass DesktopLocalizationManager : LocalizationManager {\n    override fun getCurrentLanguageCode(): String {\n        val locale = Locale.getDefault()\n        val language = locale.language\n        val country = locale.country\n        return if (country.isNotEmpty()) {\n            \"$language-$country\"\n        } else {\n            language\n        }\n    }\n\n    override fun getPrimaryLanguageCode(): String = Locale.getDefault().language\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopPackageMonitor.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport zed.rainxch.core.domain.model.DeviceApp\nimport zed.rainxch.core.domain.model.SystemPackageInfo\nimport zed.rainxch.core.domain.system.PackageMonitor\n\nclass DesktopPackageMonitor : PackageMonitor {\n    override suspend fun isPackageInstalled(packageName: String): Boolean = false\n\n    override suspend fun getInstalledPackageInfo(packageName: String): SystemPackageInfo? = null\n\n    override suspend fun getAllInstalledPackageNames(): Set<String> = setOf()\n\n    override suspend fun getAllInstalledApps(): List<DeviceApp> = emptyList()\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt",
    "content": "package zed.rainxch.core.data.services\n\nimport zed.rainxch.core.domain.system.UpdateScheduleManager\n\n/**\n * No-op implementation for Desktop — WorkManager is Android-only.\n */\nclass DesktopUpdateScheduleManager : UpdateScheduleManager {\n    override fun reschedule(intervalHours: Long) {\n        // No background scheduler on Desktop\n    }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/utils/DesktopAppLauncher.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.utils.AppLauncher\nimport java.io.File\n\nclass DesktopAppLauncher(\n    private val logger: GitHubStoreLogger,\n    private val platform: Platform,\n) : AppLauncher {\n    override suspend fun launchApp(installedApp: InstalledApp): Result<Unit> =\n        withContext(Dispatchers.IO) {\n            runCatching {\n                when (platform) {\n                    Platform.WINDOWS -> launchWindowsApp(installedApp)\n                    Platform.MACOS -> launchMacOSApp(installedApp)\n                    Platform.LINUX -> launchLinuxApp(installedApp)\n                    else -> throw Exception(\"Unsupported platform: $platform\")\n                }\n            }.onFailure { error ->\n                logger.error(\"Failed to launch app ${installedApp.appName}: ${error.message}\")\n            }\n        }\n\n    override suspend fun canLaunchApp(installedApp: InstalledApp): Boolean =\n        withContext(Dispatchers.IO) {\n            when (platform) {\n                Platform.WINDOWS -> canLaunchWindowsApp(installedApp)\n                Platform.MACOS -> canLaunchMacOSApp(installedApp)\n                Platform.LINUX -> canLaunchLinuxApp(installedApp)\n                else -> false\n            }\n        }\n\n    private fun launchWindowsApp(installedApp: InstalledApp) {\n        val programFiles =\n            listOfNotNull(\n                System.getenv(\"ProgramFiles\"),\n                System.getenv(\"ProgramFiles(x86)\"),\n                System.getenv(\"LOCALAPPDATA\"),\n            )\n\n        var launched = false\n\n        for (basePath in programFiles) {\n            val possiblePaths =\n                listOf(\n                    File(basePath, installedApp.appName),\n                    File(basePath, installedApp.repoName),\n                    File(basePath, installedApp.packageName.substringAfterLast(\".\")),\n                )\n\n            for (appDir in possiblePaths) {\n                if (appDir.exists() && appDir.isDirectory) {\n                    val exeFiles =\n                        appDir\n                            .walkTopDown()\n                            .maxDepth(3)\n                            .filter { it.extension.equals(\"exe\", ignoreCase = true) }\n                            .filter { !it.name.contains(\"uninstall\", ignoreCase = true) }\n                            .toList()\n\n                    val mainExe =\n                        exeFiles.find {\n                            it.nameWithoutExtension.equals(installedApp.appName, ignoreCase = true) ||\n                                it.nameWithoutExtension.equals(\n                                    installedApp.repoName,\n                                    ignoreCase = true,\n                                )\n                        } ?: exeFiles.firstOrNull()\n\n                    if (mainExe != null) {\n                        ProcessBuilder(\"cmd\", \"/c\", \"start\", \"\", mainExe.absolutePath)\n                            .start()\n                        launched = true\n                        logger.debug(\"Launched Windows app from: ${mainExe.absolutePath}\")\n                        break\n                    }\n                }\n            }\n            if (launched) break\n        }\n\n        if (!launched) {\n            val appNameVariations =\n                listOf(\n                    installedApp.appName,\n                    installedApp.repoName,\n                    installedApp.appName.replace(\" \", \"\"),\n                )\n\n            for (name in appNameVariations) {\n                try {\n                    ProcessBuilder(\"cmd\", \"/c\", \"start\", \"\", name)\n                        .start()\n                    launched = true\n                    logger.debug(\"Launched Windows app using shell: $name\")\n                    break\n                } catch (e: Exception) {\n                }\n            }\n        }\n\n        if (!launched) {\n            val displayName = findWindowsDisplayName(installedApp)\n            if (displayName != null) {\n                val installLocation = getWindowsInstallLocation(displayName)\n                if (installLocation != null) {\n                    val installDir = File(installLocation)\n                    val exeFiles =\n                        installDir.listFiles { file ->\n                            file.extension.equals(\"exe\", ignoreCase = true) &&\n                                !file.name.contains(\"uninstall\", ignoreCase = true)\n                        }\n\n                    val mainExe = exeFiles?.firstOrNull()\n                    if (mainExe != null) {\n                        ProcessBuilder(\"cmd\", \"/c\", \"start\", \"\", mainExe.absolutePath)\n                            .start()\n                        launched = true\n                        logger.debug(\"Launched Windows app from registry location: ${mainExe.absolutePath}\")\n                    }\n                }\n            }\n        }\n\n        if (!launched) {\n            throw Exception(\"Could not find executable for ${installedApp.appName}\")\n        }\n    }\n\n    private fun launchMacOSApp(installedApp: InstalledApp) {\n        val appName =\n            if (installedApp.appName.endsWith(\".app\")) {\n                installedApp.appName\n            } else {\n                \"${installedApp.appName}.app\"\n            }\n\n        val appPath = File(\"/Applications\", appName)\n\n        if (appPath.exists()) {\n            ProcessBuilder(\"open\", \"-a\", appPath.absolutePath).start()\n            logger.debug(\"Launched macOS app: ${appPath.absolutePath}\")\n            return\n        }\n\n        val appsDir = File(\"/Applications\")\n        val matchingApp =\n            appsDir.listFiles()?.find { file ->\n                file.isDirectory &&\n                    file.name.endsWith(\".app\") &&\n                    (\n                        file.name.contains(installedApp.appName, ignoreCase = true) ||\n                            file.name.contains(installedApp.repoName, ignoreCase = true)\n                    )\n            }\n\n        if (matchingApp != null) {\n            ProcessBuilder(\"open\", \"-a\", matchingApp.absolutePath).start()\n            logger.debug(\"Launched macOS app: ${matchingApp.absolutePath}\")\n            return\n        }\n\n        try {\n            ProcessBuilder(\"open\", \"-b\", installedApp.packageName).start()\n            logger.debug(\"Launched macOS app by bundle ID: ${installedApp.packageName}\")\n            return\n        } catch (e: Exception) {\n        }\n\n        val userAppsDir = File(System.getProperty(\"user.home\"), \"Applications\")\n        val userMatchingApp =\n            userAppsDir.listFiles()?.find { file ->\n                file.isDirectory &&\n                    file.name.endsWith(\".app\") &&\n                    (\n                        file.name.contains(installedApp.appName, ignoreCase = true) ||\n                            file.name.contains(installedApp.repoName, ignoreCase = true)\n                    )\n            }\n\n        if (userMatchingApp != null) {\n            ProcessBuilder(\"open\", \"-a\", userMatchingApp.absolutePath).start()\n            logger.debug(\"Launched macOS app from user folder: ${userMatchingApp.absolutePath}\")\n            return\n        }\n\n        throw Exception(\"Could not find app for ${installedApp.appName}\")\n    }\n\n    private fun launchLinuxApp(installedApp: InstalledApp) {\n        val commandVariations =\n            listOf(\n                installedApp.appName.lowercase().replace(\" \", \"-\"),\n                installedApp.appName.lowercase().replace(\" \", \"\"),\n                installedApp.repoName.lowercase(),\n                installedApp.packageName.substringAfterLast(\".\"),\n            )\n\n        for (command in commandVariations) {\n            try {\n                ProcessBuilder(command).start()\n                logger.debug(\"Launched Linux app with command: $command\")\n                return\n            } catch (e: Exception) {\n            }\n        }\n\n        val desktopFileDirs =\n            listOf(\n                \"/usr/share/applications\",\n                \"/usr/local/share/applications\",\n                \"${System.getProperty(\"user.home\")}/.local/share/applications\",\n            )\n\n        for (dir in desktopFileDirs) {\n            val desktopDir = File(dir)\n            if (!desktopDir.exists()) continue\n\n            val desktopFile =\n                desktopDir.listFiles()?.find { file ->\n                    file.extension.equals(\"desktop\", ignoreCase = true) &&\n                        (\n                            file.nameWithoutExtension.contains(\n                                installedApp.appName,\n                                ignoreCase = true,\n                            ) ||\n                                file.nameWithoutExtension.contains(\n                                    installedApp.repoName,\n                                    ignoreCase = true,\n                                )\n                        )\n                }\n\n            if (desktopFile != null) {\n                val execLine =\n                    desktopFile\n                        .readLines()\n                        .find {\n                            it.trim().startsWith(\"Exec=\")\n                        }?.substringAfter(\"Exec=\")\n\n                if (execLine != null) {\n                    val command = execLine.replace(Regex(\"%[fFuU]\"), \"\").trim()\n                    ProcessBuilder(\"sh\", \"-c\", command).start()\n                    logger.debug(\"Launched Linux app from .desktop file: $command\")\n                    return\n                }\n            }\n        }\n\n        try {\n            val flatpakList = executeCommand(\"flatpak\", \"list\", \"--app\")\n            if (flatpakList.contains(installedApp.appName, ignoreCase = true) ||\n                flatpakList.contains(installedApp.repoName, ignoreCase = true)\n            ) {\n                val appId =\n                    flatpakList\n                        .lines()\n                        .find {\n                            it.contains(installedApp.appName, ignoreCase = true) ||\n                                it.contains(installedApp.repoName, ignoreCase = true)\n                        }?.split(Regex(\"\\\\s+\"))\n                        ?.firstOrNull()\n\n                if (appId != null) {\n                    ProcessBuilder(\"flatpak\", \"run\", appId).start()\n                    logger.debug(\"Launched Linux app via flatpak: $appId\")\n                    return\n                }\n            }\n        } catch (e: Exception) {\n        }\n\n        if (installedApp.fileExtension.equals(\"appimage\", ignoreCase = true)) {\n            val appImageDirs =\n                listOf(\n                    \"${System.getProperty(\"user.home\")}/Applications\",\n                    \"${System.getProperty(\"user.home\")}/.local/bin\",\n                    \"/opt\",\n                )\n\n            for (dir in appImageDirs) {\n                val dirFile = File(dir)\n                val appImage =\n                    dirFile.listFiles()?.find { file ->\n                        file.extension.equals(\"appimage\", ignoreCase = true) &&\n                            file.nameWithoutExtension.contains(\n                                installedApp.appName,\n                                ignoreCase = true,\n                            )\n                    }\n\n                if (appImage != null && appImage.canExecute()) {\n                    ProcessBuilder(appImage.absolutePath).start()\n                    logger.debug(\"Launched AppImage: ${appImage.absolutePath}\")\n                    return\n                }\n            }\n        }\n\n        throw Exception(\"Could not launch ${installedApp.appName}\")\n    }\n\n    private fun canLaunchWindowsApp(installedApp: InstalledApp): Boolean {\n        val programFiles =\n            listOfNotNull(\n                System.getenv(\"ProgramFiles\"),\n                System.getenv(\"ProgramFiles(x86)\"),\n            )\n\n        for (basePath in programFiles) {\n            val appDir = File(basePath, installedApp.appName)\n            if (appDir.exists() && appDir.isDirectory) {\n                val hasExe =\n                    appDir\n                        .walkTopDown()\n                        .maxDepth(3)\n                        .any { it.extension.equals(\"exe\", ignoreCase = true) }\n                if (hasExe) return true\n            }\n        }\n\n        return false\n    }\n\n    private fun canLaunchMacOSApp(installedApp: InstalledApp): Boolean {\n        val appPath = File(\"/Applications\", \"${installedApp.appName}.app\")\n        if (appPath.exists()) return true\n\n        val appsDir = File(\"/Applications\")\n        return appsDir.listFiles()?.any { file ->\n            file.name.endsWith(\".app\") &&\n                file.name.contains(installedApp.appName, ignoreCase = true)\n        } ?: false\n    }\n\n    private fun canLaunchLinuxApp(installedApp: InstalledApp): Boolean {\n        val command = installedApp.appName.lowercase().replace(\" \", \"-\")\n        val which = executeCommand(\"which\", command)\n        if (which.isNotBlank()) return true\n\n        val desktopFileDirs =\n            listOf(\n                \"/usr/share/applications\",\n                \"/usr/local/share/applications\",\n                \"${System.getProperty(\"user.home\")}/.local/share/applications\",\n            )\n\n        return desktopFileDirs.any { dir ->\n            File(dir).listFiles()?.any { file ->\n                file.extension.equals(\"desktop\", ignoreCase = true) &&\n                    file.nameWithoutExtension.contains(installedApp.appName, ignoreCase = true)\n            } ?: false\n        }\n    }\n\n    private fun findWindowsDisplayName(installedApp: InstalledApp): String? {\n        val searchName = installedApp.appName\n        val result =\n            executeCommand(\n                \"powershell\",\n                \"-Command\",\n                \"Get-ItemProperty HKLM:\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\* | Where-Object { \\$_.DisplayName -like '*$searchName*' } | Select-Object -First 1 -ExpandProperty DisplayName\",\n            )\n        return result.trim().takeIf { it.isNotBlank() }\n    }\n\n    private fun getWindowsInstallLocation(displayName: String): String? {\n        val result =\n            executeCommand(\n                \"powershell\",\n                \"-Command\",\n                \"Get-ItemProperty HKLM:\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\* | Where-Object { \\$_.DisplayName -eq '$displayName' } | Select-Object -First 1 -ExpandProperty InstallLocation\",\n            )\n        return result.trim().takeIf { it.isNotBlank() }\n    }\n\n    private fun executeCommand(vararg command: String): String =\n        try {\n            val process =\n                ProcessBuilder(*command)\n                    .redirectErrorStream(true)\n                    .start()\n\n            val output = process.inputStream.bufferedReader().readText()\n            process.waitFor()\n            output\n        } catch (e: Exception) {\n            \"\"\n        }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/utils/DesktopBrowserHelper.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport zed.rainxch.core.domain.utils.BrowserHelper\nimport java.awt.Desktop\nimport java.net.URI\n\nclass DesktopBrowserHelper : BrowserHelper {\n    override fun openUrl(\n        url: String,\n        onFailure: (error: String) -> Unit,\n    ) {\n        val os = System.getProperty(\"os.name\").lowercase()\n\n        try {\n            when {\n                os.contains(\"linux\") -> {\n                    val processBuilder = ProcessBuilder(\"xdg-open\", url)\n                    processBuilder.redirectErrorStream(true)\n                    processBuilder.start()\n                }\n\n                Desktop.isDesktopSupported() &&\n                    Desktop\n                        .getDesktop()\n                        .isSupported(Desktop.Action.BROWSE)\n                -> {\n                    Desktop.getDesktop().browse(URI(url))\n                }\n\n                else -> {\n                    onFailure(\"Cannot open browser automatically. Please visit: $url\")\n                }\n            }\n        } catch (e: Exception) {\n            onFailure(\"Failed to open browser: ${e.message}. Please visit: $url\")\n        }\n    }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/utils/DesktopClipboardHelper.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport zed.rainxch.core.domain.utils.ClipboardHelper\nimport java.awt.Toolkit\nimport java.awt.datatransfer.DataFlavor\nimport java.awt.datatransfer.StringSelection\n\nclass DesktopClipboardHelper : ClipboardHelper {\n    override fun copy(\n        label: String,\n        text: String,\n    ) {\n        val clipboard = Toolkit.getDefaultToolkit().systemClipboard\n        clipboard.setContents(StringSelection(text), null)\n    }\n\n    override fun getText(): String? =\n        try {\n            val clipboard = Toolkit.getDefaultToolkit().systemClipboard\n            if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {\n                clipboard.getData(DataFlavor.stringFlavor) as? String\n            } else {\n                null\n            }\n        } catch (_: Exception) {\n            null\n        }\n}\n"
  },
  {
    "path": "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/utils/DesktopShareManager.kt",
    "content": "package zed.rainxch.core.data.utils\n\nimport zed.rainxch.core.domain.utils.ShareManager\nimport java.awt.Desktop\nimport java.awt.FileDialog\nimport java.awt.Frame\nimport java.awt.Toolkit\nimport java.awt.datatransfer.StringSelection\nimport java.io.File\nimport javax.swing.JFileChooser\nimport javax.swing.SwingUtilities\nimport javax.swing.filechooser.FileNameExtensionFilter\n\nclass DesktopShareManager : ShareManager {\n    override fun shareText(text: String) {\n        val clipboard = Toolkit.getDefaultToolkit().systemClipboard\n        val selection = StringSelection(text)\n        clipboard.setContents(selection, null)\n    }\n\n    override fun shareFile(fileName: String, content: String, mimeType: String) {\n        SwingUtilities.invokeLater {\n            val chooser = JFileChooser().apply {\n                dialogTitle = \"Save exported apps\"\n                selectedFile = File(fileName)\n                fileFilter = FileNameExtensionFilter(\"JSON files\", \"json\")\n            }\n\n            val result = chooser.showSaveDialog(null)\n            if (result == JFileChooser.APPROVE_OPTION) {\n                var file = chooser.selectedFile\n                if (!file.name.endsWith(\".json\")) {\n                    file = File(file.absolutePath + \".json\")\n                }\n                file.writeText(content)\n            }\n        }\n    }\n\n    override fun pickFile(mimeType: String, onResult: (String?) -> Unit) {\n        SwingUtilities.invokeLater {\n            val chooser = JFileChooser().apply {\n                dialogTitle = \"Select file to import\"\n                fileFilter = FileNameExtensionFilter(\"JSON files\", \"json\")\n            }\n\n            val result = chooser.showOpenDialog(null)\n            if (result == JFileChooser.APPROVE_OPTION) {\n                try {\n                    val content = chooser.selectedFile.readText()\n                    onResult(content)\n                } catch (e: Exception) {\n                    onResult(null)\n                }\n            } else {\n                onResult(null)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "core/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "core/domain/src/androidMain/kotlin/zed/rainxch/core/domain/Platform.android.kt",
    "content": "package zed.rainxch.core.domain\n\nimport zed.rainxch.core.domain.model.Platform\n\nactual fun getPlatform(): Platform = Platform.ANDROID\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/Platform.kt",
    "content": "package zed.rainxch.core.domain\n\nimport zed.rainxch.core.domain.model.Platform\n\nexpect fun getPlatform(): Platform\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/logging/GitHubStoreLogger.kt",
    "content": "package zed.rainxch.core.domain.logging\n\ninterface GitHubStoreLogger {\n    fun debug(message: String)\n\n    fun info(message: String)\n\n    fun warn(message: String)\n\n    fun error(\n        message: String,\n        throwable: Throwable? = null,\n    )\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ApkPackageInfo.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class ApkPackageInfo(\n    val packageName: String,\n    val versionName: String,\n    val versionCode: Long,\n    val appName: String,\n    val signingFingerprint: String?,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class AppTheme {\n    DYNAMIC,\n    OCEAN,\n    PURPLE,\n    FOREST,\n    SLATE,\n    AMBER,\n    ;\n\n    companion object {\n        fun fromName(name: String?): AppTheme = entries.find { it.name == name } ?: OCEAN\n    }\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AssetArchitectureMatcher.kt",
    "content": "package zed.rainxch.core.domain.model\n\nobject AssetArchitectureMatcher {\n    private val universalRegex =\n        Regex(\n            pattern = \"\"\"(^|[^a-z0-9])(universal|noarch|all-arch|fat)([^a-z0-9]|$)\"\"\",\n        )\n    private val x86_64Regex =\n        Regex(\n            pattern = \"\"\"(^|[^a-z0-9])(x86[_-]64|amd64|x64)([^a-z0-9]|$)\"\"\",\n        )\n    private val arm64Regex =\n        Regex(\n            pattern = \"\"\"(^|[^a-z0-9])(aarch64|arm64|arm64-v8a|armv8a|armv8l|armv8|arm-v8|v8a)([^a-z0-9]|$)\"\"\",\n        )\n    private val x86Regex =\n        Regex(\n            pattern = \"\"\"(^|[^a-z0-9])(i386|i686|x86)([^a-z0-9]|$)\"\"\",\n        )\n    private val armRegex =\n        Regex(\n            pattern = \"\"\"(^|[^a-z0-9])(armeabi-v7a|armeabi|armv7a|armv7|arm-v7|v7a|arm)([^a-z0-9]|$)\"\"\",\n        )\n\n    fun detectArchitecture(assetName: String): SystemArchitecture? {\n        val name = assetName.lowercase().replace('_', '-')\n        if (universalRegex.containsMatchIn(name)) return null\n        if (x86_64Regex.containsMatchIn(name)) return SystemArchitecture.X86_64\n        if (arm64Regex.containsMatchIn(name)) return SystemArchitecture.AARCH64\n        if (x86Regex.containsMatchIn(name)) return SystemArchitecture.X86\n        if (armRegex.containsMatchIn(name)) return SystemArchitecture.ARM\n        return null\n    }\n\n    fun isCompatible(\n        assetName: String,\n        systemArch: SystemArchitecture,\n    ): Boolean {\n        val assetArch = detectArchitecture(assetName) ?: return true\n        return when (systemArch) {\n            SystemArchitecture.X86_64 -> assetArch == SystemArchitecture.X86_64 || assetArch == SystemArchitecture.X86\n            SystemArchitecture.AARCH64 -> assetArch == SystemArchitecture.AARCH64 || assetArch == SystemArchitecture.ARM\n            SystemArchitecture.X86 -> assetArch == SystemArchitecture.X86\n            SystemArchitecture.ARM -> assetArch == SystemArchitecture.ARM\n            SystemArchitecture.UNKNOWN -> true\n        }\n    }\n\n    fun isExactMatch(\n        assetName: String,\n        systemArch: SystemArchitecture,\n    ): Boolean {\n        val assetArch = detectArchitecture(assetName) ?: return false\n        return assetArch == systemArch\n    }\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/DeviceApp.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class DeviceApp(\n    val packageName: String,\n    val appName: String,\n    val versionName: String?,\n    val versionCode: Long,\n    val signingFingerprint: String?,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/DiscoveryPlatform.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class DiscoveryPlatform {\n    All,\n    Android,\n    Macos,\n    Windows,\n    Linux,\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/DownloadProgress.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class DownloadProgress(\n    val bytesDownloaded: Long,\n    val totalBytes: Long?,\n    val percent: Int?,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ExportedApp.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ExportedApp(\n    val packageName: String,\n    val repoOwner: String,\n    val repoName: String,\n    val repoUrl: String,\n)\n\n@Serializable\ndata class ExportedAppList(\n    val version: Int = 1,\n    val exportedAt: Long = 0L,\n    val apps: List<ExportedApp> = emptyList(),\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/FavoriteRepo.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class FavoriteRepo(\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val isInstalled: Boolean = false,\n    val installedPackageName: String? = null,\n    val latestVersion: String?,\n    val latestReleaseUrl: String?,\n    val addedAt: Long,\n    val lastSyncedAt: Long,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/FontTheme.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class FontTheme(\n    val displayName: String,\n) {\n    SYSTEM(\"System\"),\n    CUSTOM(\"JetBrains Mono + Inter\"),\n    ;\n\n    companion object {\n        fun fromName(name: String?): FontTheme = entries.find { it.name == name } ?: CUSTOM\n    }\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubAsset(\n    val id: Long,\n    val name: String,\n    val contentType: String,\n    val size: Long,\n    val downloadUrl: String,\n    val uploader: GithubUser,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubDeviceStart.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class GithubDeviceStart(\n    val deviceCode: String,\n    val userCode: String,\n    val verificationUri: String,\n    val verificationUriComplete: String? = null,\n    val intervalSec: Int = 5,\n    val expiresInSec: Int,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubDeviceTokenError.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class GithubDeviceTokenError(\n    val error: String,\n    val errorDescription: String? = null,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubDeviceTokenSuccess.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class GithubDeviceTokenSuccess(\n    val accessToken: String,\n    val tokenType: String,\n    val expiresIn: Long? = null,\n    val scope: String? = null,\n    val refreshToken: String? = null,\n    val refreshTokenExpiresIn: Long? = null,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRelease.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubRelease(\n    val id: Long,\n    val tagName: String,\n    val name: String?,\n    val author: GithubUser,\n    val publishedAt: String,\n    val description: String?,\n    val assets: List<GithubAsset>,\n    val tarballUrl: String,\n    val zipballUrl: String,\n    val htmlUrl: String,\n    val isPrerelease: Boolean = false,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubRepoSummary(\n    val id: Long,\n    val name: String,\n    val fullName: String,\n    val owner: GithubUser,\n    val description: String?,\n    val defaultBranch: String,\n    val htmlUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val language: String?,\n    val topics: List<String>?,\n    val releasesUrl: String,\n    val updatedAt: String,\n    val isFork: Boolean = false,\n    val availablePlatforms: List<DiscoveryPlatform> = emptyList(),\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubUser.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubUser(\n    val id: Long,\n    val login: String,\n    val avatarUrl: String,\n    val htmlUrl: String,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubUserProfile.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubUserProfile(\n    val id: Long,\n    val login: String,\n    val name: String?,\n    val bio: String?,\n    val avatarUrl: String,\n    val htmlUrl: String,\n    val followers: Int,\n    val following: Int,\n    val publicRepos: Int,\n    val location: String?,\n    val company: String?,\n    val blog: String?,\n    val twitterUsername: String?,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstallSource.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class InstallSource {\n    THIS_APP,\n    OBTAINIUM,\n    APP_MANAGER,\n    MANUAL,\n    UNKNOWN,\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class InstalledApp(\n    val packageName: String,\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val installedVersion: String,\n    val installedAssetName: String?,\n    val installedAssetUrl: String?,\n    val latestVersion: String?,\n    val latestAssetName: String?,\n    val latestAssetUrl: String?,\n    val latestAssetSize: Long?,\n    val appName: String,\n    val installSource: InstallSource,\n    val installedAt: Long,\n    val lastCheckedAt: Long,\n    val lastUpdatedAt: Long,\n    val isUpdateAvailable: Boolean,\n    val signingFingerprint: String?,\n    val updateCheckEnabled: Boolean = true,\n    val releaseNotes: String? = \"\",\n    val systemArchitecture: String,\n    val fileExtension: String,\n    val isPendingInstall: Boolean = false,\n    val installedVersionName: String? = null,\n    val installedVersionCode: Long = 0L,\n    val latestVersionName: String? = null,\n    val latestVersionCode: Long? = null,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstallerType.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class InstallerType {\n    DEFAULT,\n    SHIZUKU;\n\n    companion object {\n        fun fromName(name: String?): InstallerType =\n            entries.find { it.name == name } ?: DEFAULT\n    }\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/PackageChangeType.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class PackageChangeType {\n    INSTALLED,\n    UNINSTALLED,\n    UPDATED,\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/PaginatedDiscoveryRepositories.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PaginatedDiscoveryRepositories(\n    val repos: List<GithubRepoSummary>,\n    val hasMore: Boolean,\n    val nextPageIndex: Int,\n    val totalCount: Int? = null,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/Platform.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class Platform {\n    ANDROID,\n    WINDOWS,\n    MACOS,\n    LINUX,\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyConfig.kt",
    "content": "package zed.rainxch.core.domain.model\n\nsealed class ProxyConfig {\n    data object None : ProxyConfig()\n\n    data object System : ProxyConfig()\n\n    data class Http(\n        val host: String,\n        val port: Int,\n        val username: String? = null,\n        val password: String? = null,\n    ) : ProxyConfig()\n\n    data class Socks(\n        val host: String,\n        val port: Int,\n        val username: String? = null,\n        val password: String? = null,\n    ) : ProxyConfig()\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RateLimitException.kt",
    "content": "package zed.rainxch.core.domain.model\n\nclass RateLimitException(\n    val rateLimitInfo: RateLimitInfo,\n) : Exception()\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RateLimitInfo.kt",
    "content": "package zed.rainxch.core.domain.model\n\nimport kotlin.time.Clock\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.Instant\n\ndata class RateLimitInfo(\n    val limit: Int,\n    val remaining: Int,\n    val resetTimestamp: Long,\n    val resource: String = \"core\",\n) {\n    val isExhausted: Boolean\n        get() = remaining == 0\n\n    fun timeUntilReset(): Duration {\n        val reset = Instant.fromEpochSeconds(resetTimestamp)\n        return (reset - Clock.System.now()).coerceAtLeast(Duration.ZERO)\n    }\n\n    fun isCurrentlyLimited(): Boolean = isExhausted && timeUntilReset() > Duration.ZERO\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ShizukuAvailability.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class ShizukuAvailability {\n    UNAVAILABLE,\n    NOT_RUNNING,\n    PERMISSION_NEEDED,\n    READY\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/StarredRepository.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class StarredRepository(\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val openIssuesCount: Int,\n    val isInstalled: Boolean = false,\n    val installedPackageName: String? = null,\n    val latestVersion: String?,\n    val latestReleaseUrl: String?,\n    val starredAt: Long?,\n    val addedAt: Long,\n    val lastSyncedAt: Long,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/SystemArchitecture.kt",
    "content": "package zed.rainxch.core.domain.model\n\nenum class SystemArchitecture {\n    X86_64, // Intel/AMD 64-bit\n    AARCH64, // ARM 64-bit\n    X86, // Intel/AMD 32-bit\n    ARM, // ARM 32-bit\n    UNKNOWN,\n    ;\n\n    companion object {\n        fun fromString(arch: String): SystemArchitecture {\n            val normalized = arch.lowercase().trim()\n            return when (normalized) {\n                in listOf(\"x86_64\", \"amd64\", \"x64\") -> X86_64\n                in listOf(\"aarch64\", \"arm64\", \"arm64-v8a\", \"armv8\", \"armv8a\", \"armv8l\") -> AARCH64\n                in listOf(\"x86\", \"i386\", \"i686\") -> X86\n                in listOf(\"arm\", \"armv7l\", \"armv7\", \"armv7a\", \"armeabi\", \"armeabi-v7a\") -> ARM\n                else -> UNKNOWN\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/SystemPackageInfo.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class SystemPackageInfo(\n    val packageName: String,\n    val versionName: String,\n    val versionCode: Long,\n    val isInstalled: Boolean,\n    val signingFingerprint: String?,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/UpdateHistory.kt",
    "content": "package zed.rainxch.core.domain.model\n\ndata class UpdateHistory(\n    val id: Long = 0,\n    val packageName: String,\n    val appName: String,\n    val repoOwner: String,\n    val repoName: String,\n    val fromVersion: String,\n    val toVersion: String,\n    val updatedAt: Long,\n    val updateSource: InstallSource,\n    val success: Boolean = true,\n    val errorMessage: String? = null,\n)\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/Downloader.kt",
    "content": "package zed.rainxch.core.domain.network\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.DownloadProgress\n\ninterface Downloader {\n    fun download(\n        url: String,\n        suggestedFileName: String? = null,\n    ): Flow<DownloadProgress>\n\n    suspend fun saveToFile(\n        url: String,\n        suggestedFileName: String? = null,\n    ): String\n\n    suspend fun getDownloadedFilePath(fileName: String): String?\n\n    suspend fun cancelDownload(fileName: String): Boolean\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AuthenticationState.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.SharedFlow\n\ninterface AuthenticationState {\n    fun isUserLoggedIn(): Flow<Boolean>\n\n    suspend fun isCurrentlyUserLoggedIn(): Boolean\n\n    val sessionExpiredEvent: SharedFlow<Unit>\n\n    suspend fun notifySessionExpired()\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/FavouritesRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.FavoriteRepo\n\ninterface FavouritesRepository {\n    fun getAllFavorites(): Flow<List<FavoriteRepo>>\n\n    fun isFavorite(repoId: Long): Flow<Boolean>\n\n    suspend fun isFavoriteSync(repoId: Long): Boolean\n\n    suspend fun toggleFavorite(repo: FavoriteRepo)\n\n    suspend fun updateFavoriteInstallStatus(\n        repoId: Long,\n        installed: Boolean,\n        packageName: String?,\n    )\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.InstalledApp\n\ninterface InstalledAppsRepository {\n    fun getAllInstalledApps(): Flow<List<InstalledApp>>\n\n    fun getAppsWithUpdates(): Flow<List<InstalledApp>>\n\n    fun getUpdateCount(): Flow<Int>\n\n    suspend fun getAppByPackage(packageName: String): InstalledApp?\n\n    suspend fun getAppByRepoId(repoId: Long): InstalledApp?\n\n    fun getAppByRepoIdAsFlow(repoId: Long): Flow<InstalledApp?>\n\n    suspend fun isAppInstalled(repoId: Long): Boolean\n\n    suspend fun saveInstalledApp(app: InstalledApp)\n\n    suspend fun deleteInstalledApp(packageName: String)\n\n    suspend fun checkForUpdates(packageName: String): Boolean\n\n    suspend fun checkAllForUpdates()\n\n    suspend fun updateAppVersion(\n        packageName: String,\n        newTag: String,\n        newAssetName: String,\n        newAssetUrl: String,\n        newVersionName: String,\n        newVersionCode: Long,\n        signingFingerprint: String?,\n    )\n\n    suspend fun updateApp(app: InstalledApp)\n\n    suspend fun updatePendingStatus(\n        packageName: String,\n        isPending: Boolean,\n    )\n\n    suspend fun <R> executeInTransaction(block: suspend () -> R): R\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.ProxyConfig\n\ninterface ProxyRepository {\n    fun getProxyConfig(): Flow<ProxyConfig>\n\n    suspend fun setProxyConfig(config: ProxyConfig)\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/RateLimitRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport zed.rainxch.core.domain.model.RateLimitInfo\n\ninterface RateLimitRepository {\n    val rateLimitState: StateFlow<RateLimitInfo?>\n    val rateLimitExhaustedEvent: SharedFlow<RateLimitInfo>\n\n    fun updateRateLimit(rateLimitInfo: RateLimitInfo?)\n\n    fun getCurrentRateLimit(): RateLimitInfo?\n\n    fun isCurrentlyLimited(): Boolean\n\n    fun clear()\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\n\ninterface SeenReposRepository {\n    fun getAllSeenRepoIds(): Flow<Set<Long>>\n\n    suspend fun markAsSeen(repoId: Long)\n\n    suspend fun clearAll()\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/StarredRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.StarredRepository\n\ninterface StarredRepository {\n    fun getAllStarred(): Flow<List<StarredRepository>>\n\n    suspend fun isStarred(repoId: Long): Boolean\n\n    suspend fun syncStarredRepos(forceRefresh: Boolean = false): Result<Unit>\n\n    suspend fun getLastSyncTime(): Long?\n\n    suspend fun needsSync(): Boolean\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt",
    "content": "package zed.rainxch.core.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.domain.model.InstallerType\n\ninterface TweaksRepository {\n    fun getThemeColor(): Flow<AppTheme>\n\n    suspend fun setThemeColor(theme: AppTheme)\n\n    fun getIsDarkTheme(): Flow<Boolean?>\n\n    suspend fun setDarkTheme(isDarkTheme: Boolean?)\n\n    fun getAmoledTheme(): Flow<Boolean>\n\n    suspend fun setAmoledTheme(enabled: Boolean)\n\n    fun getFontTheme(): Flow<FontTheme>\n\n    suspend fun setFontTheme(fontTheme: FontTheme)\n\n    fun getAutoDetectClipboardLinks(): Flow<Boolean>\n\n    suspend fun setAutoDetectClipboardLinks(enabled: Boolean)\n\n    fun getInstallerType(): Flow<InstallerType>\n\n    suspend fun setInstallerType(type: InstallerType)\n\n    fun getAutoUpdateEnabled(): Flow<Boolean>\n\n    suspend fun setAutoUpdateEnabled(enabled: Boolean)\n\n    fun getUpdateCheckInterval(): Flow<Long>\n\n    suspend fun setUpdateCheckInterval(hours: Long)\n\n    fun getIncludePreReleases(): Flow<Boolean>\n\n    suspend fun setIncludePreReleases(enabled: Boolean)\n\n    fun getLiquidGlassEnabled(): Flow<Boolean>\n\n    suspend fun setLiquidGlassEnabled(enabled: Boolean)\n\n    fun getHideSeenEnabled(): Flow<Boolean>\n\n    suspend fun setHideSeenEnabled(enabled: Boolean)\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt",
    "content": "package zed.rainxch.core.domain.system\n\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.SystemArchitecture\n\ninterface Installer {\n    suspend fun isSupported(extOrMime: String): Boolean\n\n    suspend fun ensurePermissionsOrThrow(extOrMime: String)\n\n    suspend fun install(\n        filePath: String,\n        extOrMime: String,\n    )\n\n    fun uninstall(packageName: String)\n\n    fun isAssetInstallable(assetName: String): Boolean\n\n    fun choosePrimaryAsset(assets: List<GithubAsset>): GithubAsset?\n\n    fun detectSystemArchitecture(): SystemArchitecture\n\n    fun isObtainiumInstalled(): Boolean\n\n    fun openInObtainium(\n        repoOwner: String,\n        repoName: String,\n        onOpenInstaller: () -> Unit,\n    )\n\n    fun isAppManagerInstalled(): Boolean\n\n    fun openInAppManager(\n        filePath: String,\n        onOpenInstaller: () -> Unit,\n    )\n\n    fun getApkInfoExtractor(): InstallerInfoExtractor\n\n    fun openApp(packageName: String): Boolean\n\n    fun openWithExternalInstaller(filePath: String)\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/InstallerInfoExtractor.kt",
    "content": "package zed.rainxch.core.domain.system\n\nimport zed.rainxch.core.domain.model.ApkPackageInfo\n\ninterface InstallerInfoExtractor {\n    suspend fun extractPackageInfo(filePath: String): ApkPackageInfo?\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/InstallerStatusProvider.kt",
    "content": "package zed.rainxch.core.domain.system\n\nimport kotlinx.coroutines.flow.StateFlow\nimport zed.rainxch.core.domain.model.ShizukuAvailability\n\ninterface InstallerStatusProvider {\n    val shizukuAvailability: StateFlow<ShizukuAvailability>\n    fun requestShizukuPermission()\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/PackageMonitor.kt",
    "content": "package zed.rainxch.core.domain.system\n\nimport zed.rainxch.core.domain.model.DeviceApp\nimport zed.rainxch.core.domain.model.SystemPackageInfo\n\ninterface PackageMonitor {\n    suspend fun isPackageInstalled(packageName: String): Boolean\n\n    suspend fun getInstalledPackageInfo(packageName: String): SystemPackageInfo?\n\n    suspend fun getAllInstalledPackageNames(): Set<String>\n\n    suspend fun getAllInstalledApps(): List<DeviceApp>\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt",
    "content": "package zed.rainxch.core.domain.system\n\n/**\n * Abstraction for rescheduling background update checks.\n * Android implementation delegates to WorkManager; Desktop is a no-op.\n */\ninterface UpdateScheduleManager {\n    /**\n     * Reschedules the periodic update check with a new interval.\n     * Takes effect immediately (replaces existing schedule).\n     */\n    fun reschedule(intervalHours: Long)\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt",
    "content": "package zed.rainxch.core.domain.use_cases\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.system.PackageMonitor\n\n/**\n * Use case for synchronizing installed apps state with the system package manager.\n *\n * Responsibilities:\n * 1. Remove apps from DB that are no longer installed on the system\n * 2. Migrate legacy apps missing versionName/versionCode fields\n * 3. Resolve pending installs once they appear in the system package manager\n * 4. Clean up stale pending installs (older than 24 hours)\n * 5. Detect external version changes (downgrades on rooted devices, sideloads, etc.)\n *\n * This should be called before loading or refreshing app data to ensure consistency.\n */\nclass SyncInstalledAppsUseCase(\n    private val packageMonitor: PackageMonitor,\n    private val installedAppsRepository: InstalledAppsRepository,\n    private val platform: Platform,\n    private val logger: GitHubStoreLogger,\n) {\n    companion object {\n        private const val PENDING_TIMEOUT_MS = 24 * 60 * 60 * 1000L // 24 hours\n    }\n\n    suspend operator fun invoke(): Result<Unit> =\n        withContext(Dispatchers.IO) {\n            try {\n                val installedPackageNames = packageMonitor.getAllInstalledPackageNames()\n                val appsInDb = installedAppsRepository.getAllInstalledApps().first()\n                val now = System.currentTimeMillis()\n\n                val toDelete = mutableListOf<String>()\n                val toMigrate = mutableListOf<Pair<String, MigrationResult>>()\n                val toResolvePending = mutableListOf<InstalledApp>()\n                val toDeleteStalePending = mutableListOf<String>()\n                val toSyncVersions = mutableListOf<InstalledApp>()\n\n                appsInDb.forEach { app ->\n                    val isOnSystem = installedPackageNames.contains(app.packageName)\n                    when {\n                        app.isPendingInstall -> {\n                            if (isOnSystem) {\n                                toResolvePending.add(app)\n                            } else if (now - app.installedAt > PENDING_TIMEOUT_MS) {\n                                toDeleteStalePending.add(app.packageName)\n                            }\n                        }\n\n                        !isOnSystem -> {\n                            toDelete.add(app.packageName)\n                        }\n\n                        app.installedVersionName == null -> {\n                            val migrationResult = determineMigrationData(app)\n                            toMigrate.add(app.packageName to migrationResult)\n                        }\n\n                        // Detect external version changes (downgrades on rooted devices, sideloads, etc.)\n                        isOnSystem && platform == Platform.ANDROID -> {\n                            toSyncVersions.add(app)\n                        }\n                    }\n                }\n\n                executeInTransaction {\n                    toDelete.forEach { packageName ->\n                        try {\n                            installedAppsRepository.deleteInstalledApp(packageName)\n                            logger.info(\"Removed uninstalled app: $packageName\")\n                        } catch (e: Exception) {\n                            logger.error(\"Failed to delete $packageName: ${e.message}\")\n                        }\n                    }\n\n                    toDeleteStalePending.forEach { packageName ->\n                        try {\n                            installedAppsRepository.deleteInstalledApp(packageName)\n                            logger.info(\"Removed stale pending install (>24h): $packageName\")\n                        } catch (e: Exception) {\n                            logger.error(\"Failed to delete stale pending $packageName: ${e.message}\")\n                        }\n                    }\n\n                    toResolvePending.forEach { app ->\n                        try {\n                            val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)\n                            if (systemInfo != null) {\n                                val latestVersionCode = app.latestVersionCode ?: 0L\n                                installedAppsRepository.updateApp(\n                                    app.copy(\n                                        isPendingInstall = false,\n                                        installedVersionName = systemInfo.versionName,\n                                        installedVersionCode = systemInfo.versionCode,\n                                        isUpdateAvailable = latestVersionCode > systemInfo.versionCode,\n                                    ),\n                                )\n                                logger.info(\n                                    \"Resolved pending install: ${app.packageName} (v${systemInfo.versionName}, code=${systemInfo.versionCode})\",\n                                )\n                            } else {\n                                installedAppsRepository.updatePendingStatus(app.packageName, false)\n                                logger.info(\"Resolved pending install (no system info): ${app.packageName}\")\n                            }\n                        } catch (e: Exception) {\n                            logger.error(\"Failed to resolve pending ${app.packageName}: ${e.message}\")\n                        }\n                    }\n\n                    toMigrate.forEach { (packageName, migrationResult) ->\n                        try {\n                            val app = appsInDb.find { it.packageName == packageName } ?: return@forEach\n\n                            installedAppsRepository.updateApp(\n                                app.copy(\n                                    installedVersionName = migrationResult.versionName,\n                                    installedVersionCode = migrationResult.versionCode,\n                                    latestVersionName = migrationResult.versionName,\n                                    latestVersionCode = migrationResult.versionCode,\n                                ),\n                            )\n\n                            logger.info(\n                                \"Migrated $packageName: ${migrationResult.source} \" +\n                                    \"(versionName=${migrationResult.versionName}, code=${migrationResult.versionCode})\",\n                            )\n                        } catch (e: Exception) {\n                            logger.error(\"Failed to migrate $packageName: ${e.message}\")\n                        }\n                    }\n\n                    toSyncVersions.forEach { app ->\n                        try {\n                            val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)\n                            if (systemInfo != null && systemInfo.versionCode != app.installedVersionCode) {\n                                val wasDowngrade = systemInfo.versionCode < app.installedVersionCode\n                                val latestVersionCode = app.latestVersionCode ?: 0L\n                                val isUpdateAvailable = latestVersionCode > systemInfo.versionCode\n\n                                installedAppsRepository.updateApp(\n                                    app.copy(\n                                        installedVersionName = systemInfo.versionName,\n                                        installedVersionCode = systemInfo.versionCode,\n                                        installedVersion = systemInfo.versionName,\n                                        isUpdateAvailable = isUpdateAvailable,\n                                    ),\n                                )\n\n                                val action = if (wasDowngrade) \"downgrade\" else \"external update\"\n                                logger.info(\n                                    \"Detected $action for ${app.packageName}: \" +\n                                        \"DB v${app.installedVersionName}(${app.installedVersionCode}) → \" +\n                                        \"System v${systemInfo.versionName}(${systemInfo.versionCode}), \" +\n                                        \"updateAvailable=$isUpdateAvailable\",\n                                )\n                            }\n                        } catch (e: Exception) {\n                            logger.error(\"Failed to sync version for ${app.packageName}: ${e.message}\")\n                        }\n                    }\n                }\n\n                logger.info(\n                    \"Sync completed: ${toDelete.size} deleted, ${toDeleteStalePending.size} stale pending removed, \" +\n                        \"${toResolvePending.size} pending resolved, ${toMigrate.size} migrated, \" +\n                        \"${toSyncVersions.size} version-checked\",\n                )\n\n                Result.success(Unit)\n            } catch (e: Exception) {\n                logger.error(\"Sync failed: ${e.message}\")\n                Result.failure(e)\n            }\n        }\n\n    private suspend fun determineMigrationData(app: InstalledApp): MigrationResult =\n        if (platform == Platform.ANDROID) {\n            val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)\n            if (systemInfo != null) {\n                MigrationResult(\n                    versionName = systemInfo.versionName,\n                    versionCode = systemInfo.versionCode,\n                    source = \"system package manager\",\n                )\n            } else {\n                MigrationResult(\n                    versionName = app.installedVersion,\n                    versionCode = 0L,\n                    source = \"fallback to release tag\",\n                )\n            }\n        } else {\n            MigrationResult(\n                versionName = app.installedVersion,\n                versionCode = 0L,\n                source = \"desktop fallback to release tag\",\n            )\n        }\n\n    private data class MigrationResult(\n        val versionName: String,\n        val versionCode: Long,\n        val source: String,\n    )\n}\n\nsuspend fun executeInTransaction(block: suspend () -> Unit) {\n    try {\n        block()\n    } catch (e: Exception) {\n        throw e\n    }\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/utils/AppLauncher.kt",
    "content": "package zed.rainxch.core.domain.utils\n\nimport zed.rainxch.core.domain.model.InstalledApp\n\ninterface AppLauncher {\n    suspend fun launchApp(installedApp: InstalledApp): Result<Unit>\n\n    suspend fun canLaunchApp(installedApp: InstalledApp): Boolean\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/utils/BrowserHelper.kt",
    "content": "package zed.rainxch.core.domain.utils\n\ninterface BrowserHelper {\n    fun openUrl(\n        url: String,\n        onFailure: (error: String) -> Unit = { },\n    )\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/utils/ClipboardHelper.kt",
    "content": "package zed.rainxch.core.domain.utils\n\ninterface ClipboardHelper {\n    fun copy(\n        label: String,\n        text: String,\n    )\n\n    fun getText(): String?\n}\n"
  },
  {
    "path": "core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/utils/ShareManager.kt",
    "content": "package zed.rainxch.core.domain.utils\n\ninterface ShareManager {\n    fun shareText(text: String)\n    fun shareFile(fileName: String, content: String, mimeType: String = \"application/json\")\n    fun pickFile(mimeType: String = \"application/json\", onResult: (String?) -> Unit)\n}\n"
  },
  {
    "path": "core/domain/src/jvmMain/kotlin/zed/rainxch/core/domain/Platform.jvm.kt",
    "content": "package zed.rainxch.core.domain\n\nimport zed.rainxch.core.domain.model.Platform\n\nactual fun getPlatform(): Platform =\n    when {\n        System.getProperty(\"os.name\").lowercase().contains(\"win\") -> Platform.WINDOWS\n        System.getProperty(\"os.name\").lowercase().contains(\"mac\") -> Platform.MACOS\n        else -> Platform.LINUX\n    }\n"
  },
  {
    "path": "core/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "core/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n\n                implementation(libs.bundles.landscapist)\n                implementation(libs.liquid)\n\n                implementation(libs.jetbrains.lifecycle.compose)\n                implementation(libs.kotlinx.datetime)\n                implementation(libs.kotlinx.collections.immutable)\n\n                implementation(compose.components.resources)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n            }\n        }\n    }\n}\n\ncompose.resources {\n    publicResClass = true\n    packageOfResClass = \"zed.rainxch.githubstore.core.presentation.res\"\n    generateResClass = auto\n}\n"
  },
  {
    "path": "core/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/Theme.android.kt",
    "content": "package zed.rainxch.core.presentation.theme\n\nimport android.os.Build\nimport androidx.annotation.ChecksSdkIntAtLeast\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalContext\n\n@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)\nactual fun isDynamicColorAvailable(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n\n@Composable\nactual fun getDynamicColorScheme(darkTheme: Boolean): ColorScheme? {\n    if (!isDynamicColorAvailable()) return null\n\n    val context = LocalContext.current\n    return if (darkTheme) {\n        dynamicDarkColorScheme(context)\n    } else {\n        dynamicLightColorScheme(context)\n    }\n}\n"
  },
  {
    "path": "core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.android.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport android.app.Activity\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsControllerCompat\n\n@Composable\nactual fun ApplyAndroidSystemBars(isDarkTheme: Boolean?) {\n    val context = LocalContext.current\n    val activity = context as? Activity ?: return\n\n    val isDark = isDarkTheme ?: isSystemInDarkTheme()\n\n    DisposableEffect(isDark) {\n        activity.updateSystemBars(isDark)\n        onDispose { }\n    }\n}\n\nfun Activity.updateSystemBars(isDarkTheme: Boolean) {\n    WindowCompat.setDecorFitsSystemWindows(window, false)\n\n    val controller = WindowInsetsControllerCompat(window, window.decorView)\n\n    controller.isAppearanceLightStatusBars = !isDarkTheme\n    controller.isAppearanceLightNavigationBars = !isDarkTheme\n}\n"
  },
  {
    "path": "core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/utils/isLiquidFrostAvailable.android.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport android.os.Build\nimport androidx.annotation.ChecksSdkIntAtLeast\n\n@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)\nactual fun isLiquidFrostAvailable(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/drawable/ic_github.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:viewportHeight=\"20\" android:viewportWidth=\"20\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"#000000\" android:fillType=\"evenOdd\" android:pathData=\"M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0\" android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n    \n</vector>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/drawable/ic_platform_android.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M18.447,4.106C18.941,4.353 19.141,4.953 18.894,5.447L17.72,7.796C20.307,9.604 22,12.604 22,16L22,17C22,18.105 21.105,19 20,19L4,19C2.895,19 2,18.105 2,17L2,16C2,12.604 3.693,9.604 6.28,7.796L5.106,5.447C4.859,4.953 5.059,4.353 5.553,4.106C6.047,3.859 6.647,4.059 6.894,4.553L8.028,6.82C9.246,6.292 10.589,6 12,6C13.411,6 14.755,6.292 15.972,6.82L17.106,4.553C17.353,4.059 17.953,3.859 18.447,4.106ZM7.5,12C6.672,12 6,12.672 6,13.5C6,14.328 6.672,15 7.5,15C8.328,15 9,14.328 9,13.5C9,12.672 8.328,12 7.5,12ZM16.5,12C15.672,12 15,12.672 15,13.5C15,14.328 15.672,15 16.5,15C17.328,15 18,14.328 18,13.5C18,12.672 17.328,12 16.5,12Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#008d40\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/drawable/ic_platform_linux.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"32dp\" android:viewportHeight=\"32\" android:viewportWidth=\"32\" android:width=\"32dp\">\n      \n    <path android:fillColor=\"#000000\" android:pathData=\"M15.6,8.1c0,0.1 -0.1,0.1 -0.1,0.1h-0.1c-0.1,0 -0.1,-0.1 -0.2,-0.2 0,0 -0.1,-0.1 -0.1,-0.2s0,-0.1 0.1,-0.1l0.2,0.1c0.1,0.1 0.2,0.2 0.2,0.3m-1.9,-1c0,-0.5 -0.2,-0.8 -0.5,-0.8 0,0 0,0.1 -0.1,0.1v0.2h0.3c0,0.2 0.1,0.3 0.1,0.5h0.2m3.6,-0.5c0.2,0 0.3,0.2 0.4,0.5h0.2c-0.1,-0.1 -0.1,-0.2 -0.1,-0.3 0,-0.1 0,-0.2 -0.1,-0.3 -0.1,-0.1 -0.2,-0.2 -0.3,-0.2 0,0 -0.1,0.1 -0.2,0.1 0,0.1 0.1,0.1 0.1,0.2m-3.1,1.6c-0.1,0 -0.1,0 -0.1,-0.1s0,-0.2 0.1,-0.3c0.2,0 0.3,-0.1 0.3,-0.1 0.1,0 0.1,0.1 0.1,0.1 0,0.1 -0.1,0.2 -0.3,0.4h-0.1m-1.1,-0.1c-0.4,-0.2 -0.5,-0.5 -0.5,-1 0,-0.3 0,-0.5 0.2,-0.7 0.1,-0.2 0.3,-0.3 0.5,-0.3s0.3,0.1 0.5,0.3c0.1,0.3 0.2,0.6 0.2,0.9v0.2h0.1v-0.1c0.1,0 0.1,-0.2 0.1,-0.6 0,-0.3 0,-0.6 -0.2,-0.9 -0.2,-0.3 -0.4,-0.5 -0.8,-0.5 -0.3,0 -0.6,0.2 -0.7,0.5 -0.2,0.4 -0.2,0.7 -0.2,1.2 0,0.4 0.1,0.8 0.6,1.2 0,-0.1 0.1,-0.1 0.2,-0.2m12.8,14.5c0.1,0 0.1,0 0.1,-0.1 0,-0.2 -0.1,-0.5 -0.4,-0.8 -0.3,-0.3 -0.8,-0.5 -1.4,-0.6h-0.4c-0.1,0 -0.3,0 -0.4,-0.1 0.3,-1 0.4,-1.8 0.4,-2.5 0,-1 -0.2,-1.7 -0.6,-2.4 -0.4,-0.6 -0.8,-0.9 -1.3,-1 -0.1,0.1 -0.1,0.1 -0.1,0.2 0.5,0.2 1,0.6 1.3,1.2 0.3,0.7 0.4,1.3 0.4,2.1 0,0.6 -0.1,1.4 -0.5,2.5 -0.4,0.2 -0.8,0.5 -1.1,1.1 0,0.1 0,0.1 0.1,0.1 0,0 0.1,-0.1 0.2,-0.3l0.5,-0.5c0.3,-0.2 0.5,-0.3 0.8,-0.3 0.5,0 1,0.1 1.3,0.2 0.4,0.1 0.6,0.3 0.7,0.4 0.1,0.2 0.2,0.3 0.3,0.4 0,0.3 0.1,0.4 0.1,0.4M16.5,7.7c-0.1,-0.1 -0.1,-0.3 -0.1,-0.5 0,-0.4 0,-0.6 0.2,-0.9 0.2,-0.2 0.4,-0.3 0.6,-0.3 0.3,0 0.5,0.2 0.7,0.4 0.1,0.3 0.2,0.5 0.2,0.8 0,0.5 -0.2,0.8 -0.6,0.9 0,0 0.1,0.1 0.2,0.1 0.2,0 0.3,0.1 0.5,0.2 0.1,-0.6 0.2,-1 0.2,-1.5 0,-0.6 -0.1,-1 -0.3,-1.3 -0.3,-0.3 -0.6,-0.4 -1,-0.4 -0.3,0 -0.6,0.1 -0.9,0.3 -0.2,0.3 -0.3,0.5 -0.3,0.8 0,0.5 0.1,0.9 0.3,1.3 0.1,0 0.2,0.1 0.3,0.1m1.2,1.7c-1.3,0.9 -2.4,1.3 -3.2,1.3 -0.7,0 -1.4,-0.3 -2.1,-0.8 0.1,0.2 0.2,0.4 0.3,0.5l0.6,0.6c0.4,0.4 0.9,0.6 1.4,0.6 0.7,0 1.5,-0.4 2.6,-1.1l0.9,-0.6c0.2,-0.2 0.4,-0.4 0.4,-0.7 0,-0.1 0,-0.2 -0.1,-0.2 0,-0.3 -0.5,-0.6 -1.5,-0.9 -0.9,-0.4 -1.6,-0.6 -2.1,-0.6 -0.3,0 -0.8,0.2 -1.5,0.6 -0.6,0.4 -1,0.8 -1,1.2 0,0 0.1,0.1 0.2,0.3 0.6,0.5 1.2,0.8 1.8,0.8 0.8,0 1.8,-0.4 3.2,-1.4v0.2c0.1,0.1 0.1,0.2 0.1,0.2m2.4,20.7c0.4,0.8 1.1,1.2 1.9,1.2 0.2,0 0.4,0 0.6,-0.1 0.2,0 0.4,-0.1 0.5,-0.2 0.1,-0.1 0.2,-0.1 0.3,-0.2 0.2,-0.1 0.2,-0.1 0.3,-0.2l1.7,-1.5c0.4,-0.3 0.8,-0.6 1.3,-0.9l1,-0.5c0.3,-0.1 0.5,-0.2 0.7,-0.4 0.1,-0.2 0.2,-0.3 0.2,-0.6s-0.2,-0.5 -0.4,-0.7c-0.2,-0.2 -0.4,-0.3 -0.6,-0.3 -0.2,-0.1 -0.4,-0.2 -0.7,-0.5 -0.2,-0.3 -0.4,-0.6 -0.5,-1.1l-0.1,-0.6c-0.1,-0.3 -0.1,-0.5 -0.2,-0.6H26c-0.1,0 -0.3,0.1 -0.4,0.3l-0.6,0.6c-0.1,0.2 -0.4,0.4 -0.6,0.6 -0.3,0.2 -0.6,0.3 -0.8,0.3 -0.8,0 -1.2,-0.2 -1.5,-0.7 -0.2,-0.3 -0.3,-0.7 -0.4,-1.1 -0.2,-0.2 -0.3,-0.3 -0.5,-0.3 -0.5,0 -0.7,0.5 -0.7,1.6V27.3c0,0.1 -0.1,0.3 -0.1,0.6 -0.1,0.3 -0.1,0.7 -0.1,1.1l-0.2,1.1m-14.9,-0.6c1,0.1 2.1,0.4 3.3,0.9 1.2,0.5 2,0.7 2.3,0.7 0.7,0 1.3,-0.3 1.8,-0.9 0.1,-0.2 0.1,-0.4 0.1,-0.7 0,-1 -0.6,-2.2 -1.8,-3.7l-0.7,-0.9c-0.1,-0.2 -0.3,-0.5 -0.5,-0.9s-0.4,-0.7 -0.6,-0.9c-0.1,-0.2 -0.3,-0.5 -0.6,-0.7 -0.3,-0.2 -0.6,-0.4 -0.9,-0.5 -0.4,0.1 -0.7,0.2 -0.9,0.4s-0.2,0.4 -0.2,0.6c0,0.2 -0.1,0.4 -0.2,0.4 -0.1,0.1 -0.3,0.1 -0.5,0.2h-0.6c-0.5,0 -0.9,0.1 -1.1,0.2 -0.3,0.3 -0.4,0.6 -0.4,1 0,0.2 0,0.4 0.1,0.8s0.1,0.7 0.1,0.9c0,0.4 -0.1,0.8 -0.4,1.3 -0.3,0.4 -0.4,0.8 -0.4,1 0.2,0.4 0.9,0.7 2.1,0.8m3.4,-9.3c0,-0.7 0.2,-1.5 0.6,-2.4 0.4,-0.9 0.7,-1.5 1.1,-1.9 0,-0.1 -0.1,-0.1 -0.2,-0.1l-0.1,-0.2c-0.3,0.3 -0.7,1 -1.1,2.1 -0.4,0.9 -0.7,1.8 -0.7,2.4 0,0.5 0.1,0.9 0.3,1.2 0.2,0.3 0.8,0.8 1.6,1.5l1.1,0.7c1.2,1 1.8,1.7 1.8,2.1 0,0.2 -0.1,0.4 -0.4,0.7 -0.2,0.2 -0.5,0.4 -0.7,0.4v0.1l0.3,0.6c0.4,0.6 1.4,0.9 2.6,0.9 2.3,0 4,-0.9 5.3,-2.8 0,-0.5 0,-0.8 -0.1,-1v-0.4c0,-0.7 0.1,-1.2 0.3,-1.5 0.2,-0.3 0.4,-0.5 0.7,-0.5 0.2,0 0.4,0.1 0.6,0.2 0.1,-0.8 0.1,-1.5 0.1,-2.1 0,-0.9 0,-1.7 -0.2,-2.4 -0.1,-0.6 -0.3,-1.1 -0.5,-1.5l-0.6,-0.9c-0.2,-0.3 -0.3,-0.6 -0.5,-0.9 -0.1,-0.4 -0.2,-0.7 -0.2,-1.2 -0.3,-0.5 -0.5,-1 -0.8,-1.5 -0.2,-0.5 -0.4,-1 -0.6,-1.4l-0.9,0.7c-1,0.7 -1.8,1 -2.6,1 -0.6,0 -1.1,-0.1 -1.4,-0.5l-0.6,-0.5c0,0.3 -0.1,0.7 -0.3,1.1l-0.6,1.2c-0.3,0.7 -0.4,1.1 -0.5,1.4 0,0.2 -0.1,0.4 -0.1,0.4l-0.8,1.5c-0.8,1.5 -1.3,3 -1.3,4.1 0,0.2 0,0.5 0.1,0.7 -0.5,-0.3 -0.7,-0.7 -0.7,-1.3m7.4,9.7c-1.3,0 -2.4,0.2 -3.1,0.5 -0.5,0.6 -1.1,0.9 -1.9,0.9 -0.5,0 -1.3,-0.2 -2.4,-0.6 -1.1,-0.4 -2,-0.7 -2.9,-0.8 -0.1,0 -0.3,-0.1 -0.6,-0.1s-0.6,-0.1 -0.8,-0.1c-0.2,0 -0.5,-0.1 -0.7,-0.2 -0.3,-0.1 -0.5,-0.2 -0.6,-0.3 -0.1,-0.1 -0.2,-0.3 -0.2,-0.4 0,-0.2 0,-0.3 0.1,-0.5s0.2,-0.3 0.3,-0.4c0.1,-0.1 0.1,-0.2 0.2,-0.3 0.1,-0.1 0.1,-0.2 0.1,-0.3 0,-0.1 0.1,-0.2 0.1,-0.3v-0.3s0,-0.4 -0.1,-1c-0.1,-0.5 -0.1,-0.9 -0.1,-1 0,-0.5 0.1,-0.8 0.3,-1.1 0.2,-0.3 0.4,-0.4 0.7,-0.4h1.2c0.1,0 0.2,-0.1 0.5,-0.2 0.1,-0.2 0.1,-0.3 0.2,-0.4 0.1,-0.1 0.1,-0.2 0.1,-0.3 0,-0.1 0,-0.1 0.1,-0.2 0,-0.1 0.1,-0.2 0.2,-0.2 -0.1,-0.1 -0.1,-0.2 -0.1,-0.4v-0.3c0,-0.4 0.2,-0.9 0.5,-1.6l0.4,-0.6 0.7,-1.4c0.2,-0.4 0.4,-1 0.6,-1.8 0.2,-0.7 0.6,-1.4 1.2,-2.2l0.8,-0.9c0.5,-0.6 0.9,-1.1 1.1,-1.5 0.2,-0.4 0.3,-0.9 0.3,-1.3 0,-0.2 -0.1,-0.8 -0.2,-1.8s-0.2,-2.1 -0.2,-3c0,-0.7 0.1,-1.2 0.2,-1.7s0.4,-1 0.7,-1.4c0.3,-0.4 0.7,-0.8 1.3,-1 0.6,-0.2 1.3,-0.3 2.2,-0.3 0.3,0 0.6,0 0.9,0.1 0.3,0 0.7,0.1 1.2,0.3 0.4,0.2 0.8,0.4 1.1,0.7 0.2,0.2 0.6,0.7 0.9,1.2 0.2,0.6 0.4,1.2 0.5,2.1 0.1,0.5 0.1,1 0.2,1.7 0,0.6 0.1,1 0.1,1.3 0.1,0.3 0.1,0.7 0.2,1.2 0.1,0.4 0.2,0.8 0.4,1.1 0.2,0.4 0.4,0.8 0.7,1.2 0.3,0.5 0.7,1 1.1,1.6 0.9,1 1.6,2.2 2.1,3.3 0.5,1 0.8,2.4 0.8,3.8 0,0.7 -0.1,1.4 -0.3,2.1 0.2,0 0.3,0.1 0.4,0.2s0.2,0.5 0.3,0.9l0.1,0.8c0.1,0.2 0.2,0.4 0.5,0.6 0.2,0.2 0.4,0.3 0.7,0.5 0.2,0.1 0.5,0.2 0.7,0.4 0.2,0.2 0.3,0.4 0.3,0.6 0,0.3 -0.1,0.6 -0.3,0.8 -0.2,0.2 -0.4,0.3 -0.7,0.4l-1.2,0.6c-0.5,0.3 -1,0.7 -1.5,1.1l-1,0.9c-0.4,0.4 -0.8,0.7 -1.1,0.9 -0.3,0.2 -0.7,0.3 -1.1,0.3l-0.7,-0.1c-0.8,-0.2 -1.3,-0.6 -1.6,-1.3 -1.8,0 -3.1,-0.1 -3.9,-0.1\"/>\n    \n</vector>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/drawable/ic_platform_macos.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"32dp\" android:viewportHeight=\"32\" android:viewportWidth=\"32\" android:width=\"32dp\">\n      \n    <path android:fillColor=\"#000000\" android:pathData=\"M25.658,17.49c0.052,1.257 1.082,2.032 2.649,2.032 1.65,0 2.688,-0.812 2.688,-2.107 0,-1.016 -0.586,-1.587 -1.969,-1.903l-0.784,-0.18c-0.837,-0.197 -1.181,-0.462 -1.181,-0.916 0,-0.566 0.519,-0.945 1.29,-0.945 0.779,0 1.312,0.382 1.37,1.021h1.162c-0.029,-1.2 -1.021,-2.012 -2.523,-2.012 -1.483,0 -2.537,0.816 -2.537,2.024 0,0.975 0.596,1.578 1.852,1.867l0.884,0.207c0.86,0.204 1.208,0.487 1.208,0.977 0,0.567 -0.571,0.973 -1.393,0.973 -0.831,0 -1.458,-0.411 -1.535,-1.04zM21.932,13.518c1.213,0 1.988,0.962 1.988,2.484 0,1.516 -0.775,2.479 -1.987,2.479 -1.22,0 -1.989,-0.962 -1.989,-2.479 0,-1.521 0.77,-2.484 1.988,-2.484zM21.932,12.479c-0.032,-0.001 -0.07,-0.002 -0.108,-0.002 -1.74,0 -3.15,1.41 -3.15,3.15 0,0.133 0.008,0.263 0.024,0.391l-0.002,-0.015c-0.014,0.113 -0.022,0.243 -0.022,0.375 0,1.738 1.409,3.146 3.146,3.146 0.04,0 0.079,-0.001 0.119,-0.002l-0.006,0c0.032,0.001 0.07,0.002 0.107,0.002 1.737,0 3.144,-1.408 3.144,-3.144 0,-0.133 -0.008,-0.263 -0.024,-0.392l0.002,0.015c0.014,-0.113 0.023,-0.245 0.023,-0.378 0,-1.739 -1.409,-3.148 -3.148,-3.148 -0.037,0 -0.074,0.001 -0.11,0.002l0.005,-0zM18.301,16.186c-0.087,-1.065 -0.973,-1.896 -2.053,-1.896 -0.059,0 -0.117,0.002 -0.175,0.007l0.008,-0.001c-0.021,-0.001 -0.045,-0.001 -0.07,-0.001 -1.295,0 -2.344,1.05 -2.344,2.344 0,0.093 0.005,0.185 0.016,0.276l-0.001,-0.011c-0.01,0.082 -0.016,0.177 -0.016,0.273 0,1.287 1.043,2.33 2.33,2.33 0.033,0 0.067,-0.001 0.1,-0.002l-0.005,0c0.056,0.006 0.122,0.009 0.188,0.009 1.064,0 1.936,-0.817 2.026,-1.857l0.001,-0.008h-1.11c-0.067,0.515 -0.503,0.908 -1.03,0.908 -0.021,0 -0.043,-0.001 -0.064,-0.002l0.003,0c-0.746,0 -1.227,-0.604 -1.227,-1.652 0,-1.03 0.476,-1.653 1.218,-1.653 0.013,-0.001 0.028,-0.001 0.043,-0.001 0.541,0 0.988,0.406 1.052,0.931l0,0.005zM10.422,19.488c0.009,0 0.019,0 0.03,0 0.649,0 1.217,-0.349 1.525,-0.871l0.005,-0.008h0.022v0.799h1.135v-3.449c0,-1.005 -0.809,-1.662 -2.049,-1.662 -1.276,0 -2.074,0.671 -2.126,1.606h1.091c0.094,-0.4 0.447,-0.693 0.869,-0.693 0.042,0 0.083,0.003 0.123,0.008l-0.005,-0.001c0.58,0 0.935,0.302 0.935,0.827v0.359l-1.322,0.075c-1.22,0.076 -1.904,0.61 -1.904,1.498 0.008,0.84 0.69,1.517 1.531,1.517 0.05,0 0.099,-0.002 0.147,-0.007l-0.006,0zM10.777,18.601c-0.51,0 -0.85,-0.26 -0.85,-0.671 0,-0.397 0.325,-0.652 0.892,-0.69l1.157,-0.071v0.384c-0.034,0.588 -0.519,1.052 -1.112,1.052 -0.031,0 -0.062,-0.001 -0.092,-0.004l0.004,0zM1.004,19.408h1.176v-3.065c-0.002,-0.023 -0.003,-0.049 -0.003,-0.075 0,-0.534 0.432,-0.967 0.966,-0.968h0c0.018,-0.001 0.04,-0.002 0.062,-0.002 0.458,0 0.829,0.371 0.829,0.829 0,0.027 -0.001,0.053 -0.004,0.079l0,-0.003v3.207h1.143v-3.095c-0.002,-0.023 -0.003,-0.049 -0.003,-0.076 0,-0.519 0.42,-0.939 0.939,-0.939 0.007,0 0.013,0 0.02,0l-0.001,-0c0.018,-0.001 0.039,-0.002 0.06,-0.002 0.467,0 0.845,0.378 0.845,0.845 0,0.042 -0.003,0.083 -0.009,0.124l0.001,-0.005v3.147h1.175v-3.434c0.003,-0.034 0.004,-0.074 0.004,-0.114 0,-0.859 -0.696,-1.555 -1.555,-1.555 -0.029,0 -0.058,0.001 -0.086,0.002l0.004,-0c-0.019,-0.001 -0.04,-0.001 -0.062,-0.001 -0.673,0 -1.249,0.412 -1.492,0.996l-0.004,0.011h-0.029c-0.156,-0.585 -0.681,-1.009 -1.305,-1.009 -0.032,0 -0.064,0.001 -0.096,0.003l0.004,-0c-0.014,-0 -0.029,-0.001 -0.045,-0.001 -0.631,0 -1.169,0.397 -1.378,0.954l-0.003,0.01h-0.024v-0.869h-1.128v5.007z\"/>\n    \n</vector>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/drawable/ic_platform_windows.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"32dp\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"32dp\">\n      \n    <path android:fillColor=\"#000000\" android:pathData=\"M2,12.5v6.7l8.1,1.2v-7.9M10.1,3.7L2,4.9v6.7h8.1M21.9,11.5V2L11.1,3.6v8M11.1,20.5L21.9,22v-9.5H11.1\"/>\n    \n</vector>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values/strings.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">Installed Apps</string>\n    <string name=\"navigate_back\">Navigate back</string>\n    <string name=\"check_for_updates\">Check for updates</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">Cannot launch %1$s</string>\n    <string name=\"failed_to_open\">Failed to open %1$s</string>\n    <string name=\"failed_to_update\">Failed to update %1$s: %2$s</string>\n    <string name=\"update_failed\">Update failed</string>\n    <string name=\"update_all_failed\">Update all failed: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">All apps updated successfully</string>\n    <string name=\"no_updates_available\">No updates available</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">Search your apps</string>\n    <string name=\"no_apps_found\">No apps found</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">Update All</string>\n    <string name=\"update\">Update</string>\n    <string name=\"open\">Open</string>\n    <string name=\"cancel\">Cancel</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">Checking…</string>\n    <string name=\"updated_successfully\">Updated successfully</string>\n    <string name=\"error_with_message\">Error: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">Updating %1$d of %2$d</string>\n    <string name=\"currently_updating\">Currently: %1$s</string>\n\n\n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">Waiting for authorization…</string>\n    <string name=\"signed_in\">Signed in!</string>\n    <string name=\"redirecting_message\">You can now use the app. Redirecting…</string>\n    <string name=\"try_again\">Try again</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">Error: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">Enter this code on GitHub:</string>\n    <string name=\"copy_code\">Copy the code</string>\n    <string name=\"open_github\">Open GitHub</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">Unlock the Full\\nExperience</string>\n\n    <string name=\"more_requests\">More Requests</string>\n    <string name=\"more_requests_description\">\n        Sign in to get higher API rate limits and avoid interruptions.\n    </string>\n\n    <string name=\"sign_in_with_github\">Sign in with GitHub</string>\n\n    <string name=\"error_cancelled\">Cancelled</string>\n    <string name=\"error_unknown\">Unknown error</string>\n\n    <string name=\"language_label\">Language:</string>\n\n    <string name=\"discover_repositories\">Discover Repositories</string>\n    <string name=\"search_repositories_hint\">Search repo, description…</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">Filter by Language</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">%1$d results were found</string>\n\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">Sort by</string>\n    <string name=\"close\">Close</string>\n\n    <string name=\"sort_most_stars\">Most Stars</string>\n    <string name=\"sort_most_forks\">Most Forks</string>\n    <string name=\"sort_best_match\">Best Match</string>\n\n    <string name=\"sort_order_descending\">Descending</string>\n    <string name=\"sort_order_ascending\">Ascending</string>\n    <string name=\"sort_label\">Sort</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">All Languages</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">Search failed</string>\n    <string name=\"no_repositories_found\">No repositories found</string>\n\n    <!-- Profile -->\n    <string name=\"profile_title\">Profile</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">APPEARANCE</string>\n    <string name=\"section_network\">NETWORK</string>\n    <string name=\"section_about\">ABOUT</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">Theme Color</string>\n    <string name=\"amoled_black_theme\">AMOLED Black Theme</string>\n    <string name=\"amoled_black_description\">Pure black background for dark mode</string>\n    <string name=\"selected_color\">Selected color: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">Version</string>\n    <string name=\"help_support\">Help &amp; Support</string>\n\n    <!-- Account -->\n    <string name=\"logout\">Logout</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Proxy Type</string>\n    <string name=\"proxy_none\">None</string>\n    <string name=\"proxy_system\">System</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Host</string>\n    <string name=\"proxy_port\">Port</string>\n    <string name=\"proxy_username\">Username (optional)</string>\n    <string name=\"proxy_password\">Password (optional)</string>\n    <string name=\"proxy_save\">Save Proxy</string>\n    <string name=\"proxy_saved\">Proxy settings saved</string>\n    <string name=\"proxy_system_description\">Uses your device's proxy settings</string>\n    <string name=\"proxy_port_error\">Port must be 1–65535</string>\n    <string name=\"proxy_none_description\">Direct connection, no proxy</string>\n    <string name=\"failed_to_save_proxy_settings\">Failed to save proxy settings</string>\n    <string name=\"proxy_host_required\">Proxy host is required</string>\n    <string name=\"invalid_proxy_port\">Invalid proxy port</string>\n    <string name=\"proxy_show_password\">Show password</string>\n    <string name=\"proxy_hide_password\">Hide password</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">Logged out successfully, redirecting...</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">Warning!</string>\n    <string name=\"logout_confirmation\">Are you sure you want to logout?</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">Dynamic</string>\n    <string name=\"theme_ocean\">Ocean</string>\n    <string name=\"theme_purple\">Purple</string>\n    <string name=\"theme_forest\">Forest</string>\n    <string name=\"theme_slate\">Slate</string>\n    <string name=\"theme_amber\">Amber</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">Open repository</string>\n    <string name=\"open_in_browser\">Open in browser</string>\n    <string name=\"cancel_download\">Cancel download</string>\n    <string name=\"show_install_options\">Show install options</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">Error loading details</string>\n    <string name=\"retry\">Retry</string>\n    <string name=\"no_description\">No description provided.</string>\n    <string name=\"no_release_notes\">No release notes.</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">About this app</string>\n    <string name=\"install_logs\">Install logs</string>\n    <string name=\"author\">Author</string>\n    <string name=\"whats_new\">What’s New</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">Installed</string>\n    <string name=\"update_available\">Update available</string>\n    <string name=\"not_available\">Not available</string>\n    <string name=\"install_latest\">Install latest</string>\n    <string name=\"reinstall\">Reinstall</string>\n    <string name=\"update_app\">Update app</string>\n    <string name=\"report_issue\">Report issue</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">Downloading</string>\n    <string name=\"updating\">Updating</string>\n    <string name=\"verifying\">Verifying</string>\n    <string name=\"installing\">Installing</string>\n    <string name=\"pending_install\">Pending install</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Uninstall</string>\n    <string name=\"open_app\">Open</string>\n    <string name=\"downgrade_requires_uninstall\">Downgrade requires uninstall</string>\n    <string name=\"downgrade_warning_message\">Installing version %1$s requires uninstalling the current version (%2$s) first. Your app data will be lost.</string>\n    <string name=\"uninstall_first\">Uninstall first</string>\n    <string name=\"signing_key_changed_title\">Signing key changed</string>\n    <string name=\"signing_key_changed_message\">The signing certificate for this app has changed since it was first installed.\\n\\nThis could mean the developer rotated their signing key, or the binary may have been tampered with.\\n\\nExpected: %1$s\\nReceived: %2$s</string>\n    <string name=\"install_anyway\">Install anyway</string>\n    <string name=\"verified_build\">Verified build</string>\n    <string name=\"checking_attestation\">Checking\\u2026</string>\n    <string name=\"install_version\">Install %1$s</string>\n    <string name=\"failed_to_open_app\">Failed to open %1$s</string>\n    <string name=\"failed_to_uninstall\">Failed to uninstall %1$s</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Open in Obtainium</string>\n    <string name=\"obtainium_description\">Manage updates automatically</string>\n    <string name=\"inspect_with_appmanager\">Inspect with AppManager</string>\n    <string name=\"appmanager_description\">Check permissions, trackers &amp; security</string>\n\n    <!-- Author -->\n    <string name=\"profile\">Profile</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">Forks</string>\n    <string name=\"stars\">Stars</string>\n    <string name=\"issues\">Issues</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">by %1$s</string>\n    <string name=\"installed_version\">• Installed: %1$s</string>\n    <string name=\"architecture_compatible\">Architecture compatible</string>\n    <string name=\"update_to_version\">Update to %1$s</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">Failed to load details</string>\n    <string name=\"installer_saved_downloads\">Installer was saved into the Downloads folder</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">Download started</string>\n    <string name=\"log_downloaded\">Downloaded</string>\n    <string name=\"log_update_started\">Update started</string>\n    <string name=\"log_installed\">Installed</string>\n    <string name=\"log_updated\">Updated</string>\n    <string name=\"log_cancelled\">Cancelled</string>\n    <string name=\"log_install_started\">Installation started</string>\n    <string name=\"log_error\">Error</string>\n    <string name=\"log_error_with_message\">Error: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">Preparing for AppManager</string>\n    <string name=\"log_opened_appmanager\">Opened in AppManager</string>\n    <string name=\"log_permission_blocked\">Install permission blocked by device policy</string>\n    <string name=\"log_opened_external_installer\">Opened in external installer</string>\n\n    <!-- External installer -->\n    <string name=\"install_permission_unavailable\">Install permission unavailable</string>\n    <string name=\"install_permission_blocked_message\">The APK was downloaded successfully but this device doesn\\'t allow direct installation. Would you like to open it with an external installer?</string>\n    <string name=\"open_with_external_installer\">Open with external installer</string>\n    <string name=\"external_installer_description\">Use a third-party app to install the APK</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">Error: %1$s</string>\n    <string name=\"error_asset_not_supported\">Asset type .%1$s not supported</string>\n    <string name=\"error_file_not_found\">Downloaded file not found</string>\n\n    <string name=\"home_category_trending\">Trending</string>\n    <string name=\"home_category_hot_release\">Hot release</string>\n    <string name=\"home_category_most_popular\">Most popular</string>\n\n    <string name=\"home_finding_repositories\">Finding repositories...</string>\n    <string name=\"home_loading_more\">Loading more...</string>\n    <string name=\"home_no_more_repositories\">No more repositories</string>\n    <string name=\"home_retry\">Retry</string>\n    <string name=\"home_failed_to_load_repositories\">Failed to load repositories</string>\n    <string name=\"home_view_details\">View Details</string>\n\n    <string name=\"updated_just_now\">updated just now</string>\n    <string name=\"updated_hours_ago\">updated %1$d hour(s) ago</string>\n    <string name=\"updated_yesterday\">updated yesterday</string>\n    <string name=\"updated_days_ago\">updated %1$d day(s) ago</string>\n    <string name=\"updated_on_date\">updated on %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">Rate Limit Exceeded</string>\n    <string name=\"rate_limit_used_all\">You've used all %1$d API requests.</string>\n    <string name=\"rate_limit_used_all_free\">You've used all %1$d free API requests.</string>\n    <string name=\"rate_limit_resets_in_minutes\">Resets in %1$d minutes</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Sign in to get 5,000 requests per hour instead of 60!</string>\n    <string name=\"rate_limit_sign_in\">Sign In</string>\n    <string name=\"rate_limit_ok\">OK</string>\n    <string name=\"rate_limit_close\">Close</string>\n\n    <string name=\"system_font\">System font</string>\n    <string name=\"system_font_description\">Match your device's font for better readability</string>\n\n    <string name=\"theme_light\">Light</string>\n    <string name=\"theme_dark\">Dark</string>\n    <string name=\"theme_system\">System</string>\n\n    <string name=\"added_to_favourites\">Repository added to favourites</string>\n    <string name=\"removed_from_favourites\">Repository removed from favourites</string>\n    <string name=\"add_to_favourites\">Add to favourites</string>\n    <string name=\"remove_from_favourites\">Remove from favourites</string>\n    <string name=\"favourites\">Favourites</string>\n\n    <string name=\"added_just_now\">added just now</string>\n    <string name=\"added_hours_ago\">added %1$d hour(s) ago</string>\n    <string name=\"added_yesterday\">added yesterday</string>\n    <string name=\"added_days_ago\">added %1$d day(s) ago</string>\n    <string name=\"added_on_date\">added on %1$s</string>\n\n    <string name=\"starred_repositories\">Starred Repositories</string>\n    <string name=\"repository_starred\">Repository is starred</string>\n    <string name=\"repository_not_starred\">Repository is not starred</string>\n    <string name=\"star_from_github\">You can star a repository from GitHub</string>\n    <string name=\"unstar_from_github\">You can unstar a repository from GitHub</string>\n\n    <string name=\"sign_in_required\">Sign in required</string>\n    <string name=\"sign_in_with_github_for_stars\">Sign in with GitHub to see your starred repositories</string>\n    <string name=\"no_starred_repos\">No starred repositories</string>\n    <string name=\"star_repos_hint\">Star repositories on GitHub with installable releases to see them here</string>\n    <string name=\"last_synced\">Last synced</string>\n    <string name=\"just_now\">Just now</string>\n    <string name=\"minutes_ago\">%d min ago</string>\n    <string name=\"hours_ago\">%d h ago</string>\n    <string name=\"days_ago\">%d d ago</string>\n    <string name=\"dismiss\">Dismiss</string>\n    <string name=\"sync_starred_failed\">Failed to sync starred repos</string>\n\n    <string name=\"developer_profile_title\">Developer Profile</string>\n    <string name=\"open_developer_profile\">Open developer profile</string>\n\n    <string name=\"failed_to_load_repositories\">Failed to load repositories</string>\n    <string name=\"failed_to_load_profile\">Failed to load profile</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Repositories</string>\n    <string name=\"followers\">Followers</string>\n    <string name=\"following\">Following</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Search repositories…</string>\n    <string name=\"clear_search\">Clear search</string>\n    <string name=\"filter_all\">All</string>\n    <string name=\"filter_with_releases\">With Releases</string>\n    <string name=\"filter_installed\">Installed</string>\n    <string name=\"filter_favorites\">Favorites</string>\n    <string name=\"sort\">Sort</string>\n    <string name=\"sort_recently_updated\">Recently Updated</string>\n    <string name=\"sort_name\">Name</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">repository</string>\n    <string name=\"repositories_plural\">repositories</string>\n    <string name=\"showing_x_of_y_repositories\">Showing %1$d of %2$d repositories</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">No repositories with installable releases</string>\n    <string name=\"no_installed_repos\">No installed repositories</string>\n    <string name=\"no_favorite_repos\">No favorite repositories</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">Updated %1$s</string>\n    <string name=\"has_release\">Has Release</string>\n\n    <string name=\"time_years_ago\">%1$d y ago</string>\n    <string name=\"time_months_ago\">%1$d mo ago</string>\n    <string name=\"time_days_ago\">%1$d d ago</string>\n    <string name=\"time_hours_ago\">%1$d h ago</string>\n    <string name=\"time_minutes_ago\">%1$d m ago</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">Released just now</string>\n    <string name=\"released_hours_ago\">Released %1$d hour(s) ago</string>\n    <string name=\"released_yesterday\">Released yesterday</string>\n    <string name=\"released_days_ago\">Released %1$d day(s) ago</string>\n    <string name=\"released_on_date\">Released on %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">Home</string>\n    <string name=\"bottom_nav_search_title\">Search</string>\n    <string name=\"bottom_nav_apps_title\">Apps</string>\n    <string name=\"bottom_nav_profile_title\">Profile</string>\n\n    <string name=\"forked_repository\">Fork</string>\n\n    <!-- Assets and  Version picker -->\n    <string name=\"category_stable\">Stable</string>\n    <string name=\"category_pre_release\">Pre-release</string>\n    <string name=\"category_all\">All</string>\n    <string name=\"select_version\">Select version</string>\n    <string name=\"pre_release_badge\">Pre-release</string>\n    <string name=\"latest_badge\">Latest</string>\n    <string name=\"no_version_selected\">No version</string>\n    <string name=\"versions_title\">Versions</string>\n    <string name=\"assets_title\">Assets</string>\n    <string name=\"no_assets_selected\">No Assets</string>\n    <string name=\"no_assets_in_list\">No assets associated with this release</string>\n    <string name=\"assets_selection_label\">Select Asset Option</string>\n    <string name=\"multiple_assets_info_dialog_title\">Multiple Assets Available</string>\n    <string name=\"multiple_assets_info_dialog_text\">There are multiple installable files and application downloaders available for this release. Please review the list and select the one that matches your device requirements.</string>\n    <string name=\"icon_content_description_info\">Info</string>\n\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Last checked: %1$s</string>\n    <string name=\"last_checked_never\">Never checked</string>\n    <string name=\"last_checked_just_now\">just now</string>\n    <string name=\"last_checked_minutes_ago\">%1$d min ago</string>\n    <string name=\"last_checked_hours_ago\">%1$d h ago</string>\n    <string name=\"checking_for_updates\">Checking for updates…</string>\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Track this app</string>\n    <string name=\"app_tracked_successfully\">App added to tracking list</string>\n    <string name=\"failed_to_track_app\">Failed to track app: %1$s</string>\n    <string name=\"already_tracked\">App is already being tracked</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">Sign in to GitHub</string>\n    <string name=\"profile_sign_in_description\">Unlock the full experience. Manage your apps, sync your preferences, and browse faster.</string>\n    <string name=\"profile_repos\">Repos</string>\n    <string name=\"profile_login\">Login</string>\n    <string name=\"profile_stars_description\">Your Starred Repositories from GitHub</string>\n    <string name=\"profile_favourites_description\">Your Favourite Repositories saved locally</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Session Expired</string>\n    <string name=\"session_expired_message\">Your GitHub session has expired or the token was revoked. Please sign in again to continue using authenticated features.</string>\n    <string name=\"session_expired_hint\">You can still browse as a guest with limited API requests.</string>\n    <string name=\"sign_in_again\">Sign In Again</string>\n    <string name=\"continue_as_guest\">Continue as Guest</string>\n\n    <!-- Logout improvements -->\n    <string name=\"logout_revocation_note\">This will clear your local session and cached data. To fully revoke access, visit GitHub Settings > Applications.</string>\n\n    <!-- Auth UI improvements -->\n    <string name=\"auth_code_expires_in\">Code expires in %1$s</string>\n    <string name=\"auth_error_code_expired\">The device code has expired.</string>\n    <string name=\"auth_hint_try_again\">Please try signing in again to get a new code.</string>\n    <string name=\"auth_hint_check_connection\">Please check your internet connection and try again.</string>\n    <string name=\"auth_hint_denied\">You denied the authorization request. Try again if this was unintentional.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Read More</string>\n    <string name=\"show_less\">Show Less</string>\n\n    <string name=\"share_repository\">Share repository</string>\n    <string name=\"failed_to_share_link\">Failed to share link</string>\n    <string name=\"link_copied_to_clipboard\">Link copied to clipboard</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Translate</string>\n    <string name=\"translating\">Translating…</string>\n    <string name=\"show_original\">Show original</string>\n    <string name=\"translated_to\">Translated to %1$s</string>\n    <string name=\"translate_to\">Translate to…</string>\n    <string name=\"search_language\">Search language</string>\n    <string name=\"change_language\">Change language</string>\n    <string name=\"translation_failed\">Translation failed. Please try again.</string>\n    <string name=\"translation_error_retry\">Retry</string>\n    <string name=\"translated_from\">Auto-detected: %1$s</string>\n    <string name=\"select_language\">Select language</string>\n\n    <!-- Search - GitHub Link -->\n    <string name=\"open_github_link\">Open GitHub Link</string>\n    <string name=\"clipboard_link_detected\">GitHub link detected in clipboard</string>\n    <string name=\"auto_detect_clipboard_links\">Auto-detect clipboard links</string>\n    <string name=\"auto_detect_clipboard_description\">Automatically detect GitHub links from clipboard when opening search</string>\n    <string name=\"detected_links\">Detected Links</string>\n    <string name=\"open_in_app\">Open in app</string>\n    <string name=\"no_github_link_in_clipboard\">No GitHub link found in clipboard</string>\n\n    <string name=\"storage\">Storage</string>\n    <string name=\"clear_cache\">Clear cache</string>\n    <string name=\"current_size\">Current size:</string>\n    <string name=\"clear\">Clear</string>\n    <string name=\"cache_cleared\">The cache is gradually cleared.</string>\n\n    <string name=\"sponsor_title\">Support GitHub Store</string>\n    <string name=\"sponsor_button\">Support the project</string>\n    <string name=\"sponsor_hero_title\">Built with love,\\nmaintained with coffee</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store has reached over 130,000 downloads and 7,700 GitHub stars — 100% free, with no ads and no tracking.</string>\n\n    <string name=\"sponsor_personal_note\">I developed and maintain this project completely on my own while finishing school. Your support — even a small one — helps pay for infrastructure and continue developing the app.</string>\n\n    <string name=\"sponsor_kodee_title\">Vote for GitHub Store!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store has been nominated for the Golden Kodee Awards at KotlinConf 2026.</string>\n\n    <string name=\"sponsor_kodee_register\">1. Register</string>\n    <string name=\"sponsor_kodee_vote\">2. Vote</string>\n    <string name=\"sponsor_kodee_deadline\">Voting until March 22</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Register on the platform (Sign in with Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. Click “Vote” below</string>\n    <string name=\"sponsor_kodee_step3\">3. Find Usmon Narzullayev and click “Vote”</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">Recurring or one-time support via GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Quick one-time support</string>\n\n    <string name=\"sponsor_other_ways_title\">OTHER WAYS TO HELP</string>\n\n    <string name=\"sponsor_star_repo\">Star the repository</string>\n    <string name=\"sponsor_star_repo_desc\">Helps others discover GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">Report a bug</string>\n    <string name=\"sponsor_report_bugs_desc\">Helps make the app better</string>\n\n    <string name=\"sponsor_share\">Share with friends</string>\n    <string name=\"sponsor_share_desc\">Tell other developers about it</string>\n\n    <string name=\"sponsor_thank_you\">Any support — financial or not — helps keep the project alive. Thank you!</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Installation</string>\n    <string name=\"installer_type_default\">Default</string>\n    <string name=\"installer_type_default_description\">Standard system install dialog</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Silent install without prompts</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku is not installed</string>\n    <string name=\"shizuku_status_not_running\">Shizuku is not running</string>\n    <string name=\"shizuku_status_permission_needed\">Permission required</string>\n    <string name=\"shizuku_status_ready\">Ready</string>\n    <string name=\"shizuku_grant_permission\">Grant Permission</string>\n    <string name=\"shizuku_install_hint\">Install Shizuku to enable silent installs</string>\n    <string name=\"shizuku_start_hint\">Start Shizuku to enable silent installs</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizuku install failed, using standard installer</string>\n    <string name=\"auto_update_title\">Auto-update apps</string>\n    <string name=\"auto_update_description\">Automatically download and install updates in background via Shizuku</string>\n\n    <string name=\"section_updates\">Updates</string>\n    <string name=\"update_check_interval_title\">Update check interval</string>\n    <string name=\"update_check_interval_description\">How often to check for app updates in background</string>\n    <string name=\"interval_3h\">3h</string>\n    <string name=\"interval_6h\">6h</string>\n    <string name=\"interval_12h\">12h</string>\n    <string name=\"interval_24h\">24h</string>\n\n    <string name=\"add_by_link\">Add by link</string>\n    <string name=\"link_app_title\">Link app to repository</string>\n    <string name=\"pick_installed_app\">Pick an installed app to link with a GitHub repository</string>\n    <string name=\"search_apps_hint\">Search apps…</string>\n    <string name=\"enter_repo_url\">GitHub repository URL</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Validating…</string>\n    <string name=\"link_and_track\">Link &amp; Track</string>\n    <string name=\"checking_release\">Checking latest release…</string>\n    <string name=\"downloading_for_verification\">Downloading APK for verification…</string>\n    <string name=\"verifying_signing_key\">Verifying signing key…</string>\n    <string name=\"package_name_mismatch\">Package name mismatch: the APK is %1$s, but the selected app is %2$s</string>\n    <string name=\"signing_key_mismatch_link\">Signing key mismatch: the APK in this repository was signed by a different developer than the installed app</string>\n    <string name=\"select_asset_title\">Select installer</string>\n    <string name=\"select_asset_description\">Pick the APK to verify against your installed app</string>\n    <string name=\"download_failed\">Download failed</string>\n    <string name=\"export_apps\">Export</string>\n    <string name=\"import_apps\">Import</string>\n    <string name=\"import_apps_title\">Import apps</string>\n    <string name=\"import_apps_description\">Paste the exported JSON to restore your tracked apps</string>\n    <string name=\"import_apps_hint\">Paste exported JSON here…</string>\n\n    <string name=\"include_pre_releases_title\">Include pre-releases</string>\n    <string name=\"include_pre_releases_description\">Track pre-release versions when checking for updates. When disabled, only stable releases are considered.</string>\n    <string name=\"confirm_uninstall_title\">Uninstall app?</string>\n    <string name=\"confirm_uninstall_message\">Are you sure you want to uninstall %1$s? This action cannot be undone and app data may be lost.</string>\n\n    <string name=\"invalid_github_url\">Invalid GitHub URL. Use format: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Repository not found: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub API rate limit exceeded. Try again later.</string>\n    <string name=\"failed_to_link\">Failed to link: %1$s</string>\n    <string name=\"failed_to_load_apps\">Failed to load installed apps</string>\n    <string name=\"app_linked_success\">%1$s linked to %2$s/%3$s</string>\n    <string name=\"export_failed\">Export failed: %1$s</string>\n    <string name=\"import_failed\">Import failed: %1$s</string>\n    <string name=\"imported_apps_summary\">Imported %1$d apps</string>\n    <string name=\"imported_skipped\">, %1$d skipped</string>\n    <string name=\"imported_failed\">, %1$d failed</string>\n    <string name=\"update_package_mismatch\">Package mismatch: the APK is %1$s, but the installed app is %2$s. Update blocked.</string>\n    <string name=\"update_signing_key_mismatch\">Signing key mismatch: the update was signed by a different developer. Update blocked.</string>\n\n    <string name=\"liquid_glass_option_title\">Liquid Glass Effect</string>\n    <string name=\"liquid_glass_option_description\">Enhance the interface with a smooth glass-like appearance</string>\n\n    <string name=\"hide_seen_title\">Hide Seen Repositories</string>\n    <string name=\"hide_seen_description\">Hide repositories you have already viewed from discovery feeds</string>\n    <string name=\"clear_seen_history\">Clear Seen History</string>\n    <string name=\"clear_seen_history_description\">Reset all seen repositories so they appear again in feeds</string>\n    <string name=\"seen_history_cleared\">Seen history cleared</string>\n    <string name=\"seen_badge\">Viewed</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">التطبيقات المثبتة</string>\n    <string name=\"navigate_back\">العودة</string>\n    <string name=\"check_for_updates\">التحقق من التحديثات</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">تعذر تشغيل %1$s</string>\n    <string name=\"failed_to_open\">فشل فتح %1$s</string>\n    <string name=\"failed_to_update\">فشل تحديث %1$s: %2$s</string>\n    <string name=\"update_failed\">فشل التحديث</string>\n    <string name=\"update_all_failed\">فشل تحديث الكل: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">تم تحديث جميع التطبيقات بنجاح</string>\n    <string name=\"no_updates_available\">لا توجد تحديثات متاحة</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">ابحث في تطبيقاتك</string>\n    <string name=\"no_apps_found\">لم يتم العثور على تطبيقات</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">تحديث الكل</string>\n    <string name=\"update\">تحديث</string>\n    <string name=\"open\">فتح</string>\n    <string name=\"cancel\">إلغاء</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">جارٍ التحقق…</string>\n    <string name=\"updated_successfully\">تم التحديث بنجاح</string>\n    <string name=\"error_with_message\">خطأ: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">جارٍ تحديث %1$d من %2$d</string>\n    <string name=\"currently_updating\">الحالي: %1$s</string>\n\n\n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">في انتظار التفويض…</string>\n    <string name=\"signed_in\">تم تسجيل الدخول!</string>\n    <string name=\"redirecting_message\">يمكنك الآن استخدام التطبيق. جارٍ إعادة التوجيه…</string>\n    <string name=\"try_again\">حاول مرة أخرى</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">خطأ: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">أدخل هذا الرمز على GitHub:</string>\n    <string name=\"copy_code\">نسخ الرمز</string>\n    <string name=\"open_github\">فتح GitHub</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">افتح التجربة\\nالكاملة</string>\n\n    <string name=\"more_requests\">المزيد من الطلبات</string>\n    <string name=\"more_requests_description\">\n        سجّل الدخول للحصول على حدود أعلى لطلبات API وتجنب الانقطاعات.\n    </string>\n\n    <string name=\"sign_in_with_github\">تسجيل الدخول بـ GitHub</string>\n\n    <string name=\"error_cancelled\">تم الإلغاء</string>\n    <string name=\"error_unknown\">خطأ غير معروف</string>\n\n    <string name=\"language_label\">اللغة:</string>\n\n    <string name=\"discover_repositories\">اكتشف المستودعات</string>\n    <string name=\"search_repositories_hint\">ابحث عن مستودع، وصف…</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">تصفية حسب اللغة</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">تم العثور على %1$d نتيجة</string>\n\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">ترتيب حسب</string>\n    <string name=\"close\">إغلاق</string>\n\n    <string name=\"sort_most_stars\">الأكثر نجوماً</string>\n    <string name=\"sort_most_forks\">الأكثر تفرعاً</string>\n    <string name=\"sort_best_match\">الأفضل تطابقاً</string>\n\n    <string name=\"sort_order_descending\">تنازلي</string>\n    <string name=\"sort_order_ascending\">تصاعدي</string>\n    <string name=\"sort_label\">ترتيب</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">جميع اللغات</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">فشل البحث</string>\n    <string name=\"no_repositories_found\">لم يتم العثور على مستودعات</string>\n\n    <!-- Profile -->\n    <string name=\"profile_title\">الملف الشخصي</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">المظهر</string>\n    <string name=\"section_network\">الشبكة</string>\n    <string name=\"section_about\">حول</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">لون السمة</string>\n    <string name=\"amoled_black_theme\">سمة AMOLED السوداء</string>\n    <string name=\"amoled_black_description\">خلفية سوداء نقية للوضع الداكن</string>\n    <string name=\"selected_color\">اللون المحدد: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">الإصدار</string>\n    <string name=\"help_support\">المساعدة والدعم</string>\n\n    <!-- Account -->\n    <string name=\"logout\">تسجيل الخروج</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">نوع الوكيل</string>\n    <string name=\"proxy_none\">بدون</string>\n    <string name=\"proxy_system\">النظام</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">المضيف</string>\n    <string name=\"proxy_port\">المنفذ</string>\n    <string name=\"proxy_username\">اسم المستخدم (اختياري)</string>\n    <string name=\"proxy_password\">كلمة المرور (اختياري)</string>\n    <string name=\"proxy_save\">حفظ الوكيل</string>\n    <string name=\"proxy_saved\">تم حفظ إعدادات الوكيل</string>\n    <string name=\"proxy_system_description\">يستخدم إعدادات الوكيل الخاصة بجهازك</string>\n    <string name=\"proxy_port_error\">يجب أن يكون المنفذ بين 1–65535</string>\n    <string name=\"proxy_none_description\">اتصال مباشر، بدون وكيل</string>\n    <string name=\"failed_to_save_proxy_settings\">فشل حفظ إعدادات الوكيل</string>\n    <string name=\"proxy_host_required\">المضيف مطلوب</string>\n    <string name=\"invalid_proxy_port\">منفذ الوكيل غير صالح</string>\n    <string name=\"proxy_show_password\">إظهار كلمة المرور</string>\n    <string name=\"proxy_hide_password\">إخفاء كلمة المرور</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">تم تسجيل الخروج بنجاح، جارٍ إعادة التوجيه...</string>\n    <string name=\"cache_cleared\">تم مسح ذاكرة التخزين المؤقت بنجاح</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">تحذير!</string>\n    <string name=\"logout_confirmation\">هل أنت متأكد أنك تريد تسجيل الخروج؟</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">ديناميكي</string>\n    <string name=\"theme_ocean\">محيط</string>\n    <string name=\"theme_purple\">بنفسجي</string>\n    <string name=\"theme_forest\">غابة</string>\n    <string name=\"theme_slate\">رمادي</string>\n    <string name=\"theme_amber\">كهرماني</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">فتح المستودع</string>\n    <string name=\"open_in_browser\">فتح في المتصفح</string>\n    <string name=\"cancel_download\">إلغاء التنزيل</string>\n    <string name=\"show_install_options\">عرض خيارات التثبيت</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">خطأ في تحميل التفاصيل</string>\n    <string name=\"retry\">إعادة المحاولة</string>\n    <string name=\"no_description\">لا يوجد وصف.</string>\n    <string name=\"no_release_notes\">لا توجد ملاحظات إصدار.</string>\n    <string name=\"report_issue\">الإبلاغ عن مشكلة</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">حول هذا التطبيق</string>\n    <string name=\"install_logs\">سجلات التثبيت</string>\n    <string name=\"author\">المؤلف</string>\n    <string name=\"whats_new\">ما الجديد</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">مثبت</string>\n    <string name=\"update_available\">تحديث متاح</string>\n    <string name=\"not_available\">غير متاح</string>\n    <string name=\"install_latest\">تثبيت الأحدث</string>\n    <string name=\"reinstall\">إعادة التثبيت</string>\n    <string name=\"update_app\">تحديث التطبيق</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">جارٍ التنزيل</string>\n    <string name=\"updating\">جارٍ التحديث</string>\n    <string name=\"verifying\">جارٍ التحقق</string>\n    <string name=\"installing\">جارٍ التثبيت</string>\n    <string name=\"pending_install\">في انتظار التثبيت</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">إزالة التثبيت</string>\n    <string name=\"open_app\">فتح</string>\n    <string name=\"downgrade_requires_uninstall\">الرجوع لإصدار أقدم يتطلب إزالة التثبيت</string>\n    <string name=\"downgrade_warning_message\">تثبيت الإصدار %1$s يتطلب إزالة الإصدار الحالي (%2$s) أولاً. سيتم فقدان بيانات التطبيق.</string>\n    <string name=\"uninstall_first\">إزالة التثبيت أولاً</string>\n    <string name=\"install_version\">تثبيت %1$s</string>\n    <string name=\"failed_to_open_app\">فشل فتح %1$s</string>\n    <string name=\"failed_to_uninstall\">فشل إزالة تثبيت %1$s</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">فتح في Obtainium</string>\n    <string name=\"obtainium_description\">إدارة التحديثات تلقائياً</string>\n    <string name=\"inspect_with_appmanager\">فحص بـ AppManager</string>\n    <string name=\"appmanager_description\">فحص الأذونات والمتتبعات والأمان</string>\n\n    <!-- Author -->\n    <string name=\"profile\">الملف الشخصي</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">التفرعات</string>\n    <string name=\"stars\">النجوم</string>\n    <string name=\"issues\">المشكلات</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">بواسطة %1$s</string>\n    <string name=\"installed_version\">• المثبت: %1$s</string>\n    <string name=\"architecture_compatible\">متوافق مع المعمارية</string>\n    <string name=\"update_to_version\">التحديث إلى %1$s</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">فشل تحميل التفاصيل</string>\n    <string name=\"installer_saved_downloads\">تم حفظ المثبت في مجلد التنزيلات</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">بدأ التنزيل</string>\n    <string name=\"log_downloaded\">تم التنزيل</string>\n    <string name=\"log_update_started\">بدأ التحديث</string>\n    <string name=\"log_installed\">تم التثبيت</string>\n    <string name=\"log_updated\">تم التحديث</string>\n    <string name=\"log_cancelled\">تم الإلغاء</string>\n    <string name=\"log_install_started\">بدأ التثبيت</string>\n    <string name=\"log_error\">خطأ</string>\n    <string name=\"log_error_with_message\">خطأ: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">التحضير لـ AppManager</string>\n    <string name=\"log_opened_appmanager\">تم الفتح في AppManager</string>\n    <string name=\"log_permission_blocked\">تم حظر إذن التثبيت بواسطة سياسة الجهاز</string>\n    <string name=\"log_opened_external_installer\">تم الفتح في المثبت الخارجي</string>\n\n    <!-- External installer -->\n    <string name=\"install_permission_unavailable\">إذن التثبيت غير متاح</string>\n    <string name=\"install_permission_blocked_message\">تم تنزيل ملف APK بنجاح لكن هذا الجهاز لا يسمح بالتثبيت المباشر. هل تريد فتحه بمثبت خارجي؟</string>\n    <string name=\"open_with_external_installer\">فتح بمثبت خارجي</string>\n    <string name=\"external_installer_description\">استخدم تطبيقاً خارجياً لتثبيت ملف APK</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">خطأ: %1$s</string>\n    <string name=\"error_asset_not_supported\">نوع الملف .%1$s غير مدعوم</string>\n    <string name=\"error_file_not_found\">لم يتم العثور على الملف المنزّل</string>\n\n    <string name=\"home_category_trending\">الرائج</string>\n    <string name=\"home_category_hot_release\">إصدار ساخن</string>\n    <string name=\"home_category_most_popular\">الأكثر شعبية</string>\n\n    <string name=\"home_finding_repositories\">جارٍ البحث عن مستودعات...</string>\n    <string name=\"home_loading_more\">جارٍ تحميل المزيد...</string>\n    <string name=\"home_no_more_repositories\">لا مزيد من المستودعات</string>\n    <string name=\"home_retry\">إعادة المحاولة</string>\n    <string name=\"home_failed_to_load_repositories\">فشل تحميل المستودعات</string>\n    <string name=\"home_view_details\">عرض التفاصيل</string>\n\n    <string name=\"updated_just_now\">تم التحديث للتو</string>\n    <string name=\"updated_hours_ago\">تم التحديث منذ %1$d ساعة</string>\n    <string name=\"updated_yesterday\">تم التحديث أمس</string>\n    <string name=\"updated_days_ago\">تم التحديث منذ %1$d يوم</string>\n    <string name=\"updated_on_date\">تم التحديث في %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">تم تجاوز حد الطلبات</string>\n    <string name=\"rate_limit_used_all\">لقد استخدمت جميع طلبات API البالغة %1$d.</string>\n    <string name=\"rate_limit_used_all_free\">لقد استخدمت جميع الطلبات المجانية البالغة %1$d.</string>\n    <string name=\"rate_limit_resets_in_minutes\">يتم إعادة التعيين خلال %1$d دقيقة</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 سجّل الدخول للحصول على 5,000 طلب في الساعة بدلاً من 60!</string>\n    <string name=\"rate_limit_sign_in\">تسجيل الدخول</string>\n    <string name=\"rate_limit_ok\">حسناً</string>\n    <string name=\"rate_limit_close\">إغلاق</string>\n\n    <string name=\"system_font\">خط النظام</string>\n    <string name=\"system_font_description\">مطابقة خط جهازك لقراءة أفضل</string>\n\n    <string name=\"theme_light\">فاتح</string>\n    <string name=\"theme_dark\">داكن</string>\n    <string name=\"theme_system\">النظام</string>\n\n    <string name=\"added_to_favourites\">تمت إضافة المستودع إلى المفضلة</string>\n    <string name=\"removed_from_favourites\">تمت إزالة المستودع من المفضلة</string>\n    <string name=\"add_to_favourites\">إضافة إلى المفضلة</string>\n    <string name=\"remove_from_favourites\">إزالة من المفضلة</string>\n    <string name=\"favourites\">المفضلة</string>\n\n    <string name=\"added_just_now\">أُضيف للتو</string>\n    <string name=\"added_hours_ago\">أُضيف منذ %1$d ساعة</string>\n    <string name=\"added_yesterday\">أُضيف أمس</string>\n    <string name=\"added_days_ago\">أُضيف منذ %1$d يوم</string>\n    <string name=\"added_on_date\">أُضيف في %1$s</string>\n\n    <string name=\"starred_repositories\">المستودعات المميزة بنجمة</string>\n    <string name=\"repository_starred\">المستودع مميز بنجمة</string>\n    <string name=\"repository_not_starred\">المستودع غير مميز بنجمة</string>\n    <string name=\"star_from_github\">يمكنك تمييز مستودع بنجمة من GitHub</string>\n    <string name=\"unstar_from_github\">يمكنك إزالة النجمة عن مستودع من GitHub</string>\n\n    <string name=\"sign_in_required\">تسجيل الدخول مطلوب</string>\n    <string name=\"sign_in_with_github_for_stars\">سجّل الدخول بـ GitHub لرؤية مستودعاتك المميزة بنجمة</string>\n    <string name=\"no_starred_repos\">لا توجد مستودعات مميزة بنجمة</string>\n    <string name=\"star_repos_hint\">ميّز المستودعات بنجمة على GitHub التي تحتوي على إصدارات قابلة للتثبيت لتظهر هنا</string>\n    <string name=\"last_synced\">آخر مزامنة</string>\n    <string name=\"just_now\">الآن</string>\n    <string name=\"minutes_ago\">منذ %1$d دقيقة</string>\n    <string name=\"hours_ago\">منذ %1$d ساعة</string>\n    <string name=\"days_ago\">منذ %1$d يوم</string>\n    <string name=\"dismiss\">تجاهل</string>\n    <string name=\"sync_starred_failed\">فشلت مزامنة المستودعات المميزة بنجمة</string>\n\n    <string name=\"developer_profile_title\">الملف الشخصي للمطور</string>\n    <string name=\"open_developer_profile\">فتح الملف الشخصي للمطور</string>\n\n    <string name=\"failed_to_load_repositories\">فشل تحميل المستودعات</string>\n    <string name=\"failed_to_load_profile\">فشل تحميل الملف الشخصي</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">المستودعات</string>\n    <string name=\"followers\">المتابعون</string>\n    <string name=\"following\">يتابع</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">البحث في المستودعات…</string>\n    <string name=\"clear_search\">مسح البحث</string>\n    <string name=\"filter_all\">الكل</string>\n    <string name=\"filter_with_releases\">مع إصدارات</string>\n    <string name=\"filter_installed\">المثبتة</string>\n    <string name=\"filter_favorites\">المفضلة</string>\n    <string name=\"sort\">ترتيب</string>\n    <string name=\"sort_recently_updated\">المحدّث مؤخراً</string>\n    <string name=\"sort_name\">الاسم</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">مستودع</string>\n    <string name=\"repositories_plural\">مستودعات</string>\n    <string name=\"showing_x_of_y_repositories\">عرض %1$d من %2$d مستودع</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">لا توجد مستودعات بإصدارات قابلة للتثبيت</string>\n    <string name=\"no_installed_repos\">لا توجد مستودعات مثبتة</string>\n    <string name=\"no_favorite_repos\">لا توجد مستودعات مفضلة</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">تم التحديث %1$s</string>\n    <string name=\"has_release\">يحتوي على إصدار</string>\n\n    <string name=\"time_years_ago\">منذ %1$d سنة</string>\n    <string name=\"time_months_ago\">منذ %1$d شهر</string>\n    <string name=\"time_days_ago\">منذ %1$d يوم</string>\n    <string name=\"time_hours_ago\">منذ %1$d ساعة</string>\n    <string name=\"time_minutes_ago\">منذ %1$d دقيقة</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$d مليون</string>\n    <string name=\"count_thousands\">%1$d ألف</string>\n\n    <string name=\"released_just_now\">صدر للتو</string>\n    <string name=\"released_hours_ago\">صدر منذ %1$d ساعة</string>\n    <string name=\"released_yesterday\">صدر أمس</string>\n    <string name=\"released_days_ago\">صدر منذ %1$d يوم</string>\n    <string name=\"released_on_date\">صدر في %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">الرئيسية</string>\n    <string name=\"bottom_nav_search_title\">البحث</string>\n    <string name=\"bottom_nav_apps_title\">التطبيقات</string>\n    <string name=\"bottom_nav_profile_title\">الملف الشخصي</string>\n\n    <string name=\"forked_repository\">نسخة متفرعة</string>\n\n    <!-- Version picker -->\n    <string name=\"category_stable\">مستقر</string>\n    <string name=\"category_pre_release\">إصدار تجريبي</string>\n    <string name=\"category_all\">الكل</string>\n    <string name=\"select_version\">اختر الإصدار</string>\n    <string name=\"pre_release_badge\">تجريبي</string>\n    <string name=\"latest_badge\">الأحدث</string>\n    <string name=\"no_version_selected\">لا يوجد إصدار</string>\n    <string name=\"versions_title\">الإصدارات</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">آخر فحص: %1$s</string>\n    <string name=\"last_checked_never\">لم يتم الفحص مطلقاً</string>\n    <string name=\"last_checked_just_now\">الآن</string>\n    <string name=\"last_checked_minutes_ago\">منذ %1$d دقيقة</string>\n    <string name=\"last_checked_hours_ago\">منذ %1$d ساعة</string>\n    <string name=\"checking_for_updates\">جارٍ التحقق من التحديثات…</string>\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">تتبع هذا التطبيق</string>\n    <string name=\"app_tracked_successfully\">تمت إضافة التطبيق إلى قائمة التتبع</string>\n    <string name=\"failed_to_track_app\">فشل تتبع التطبيق: %1$s</string>\n    <string name=\"already_tracked\">التطبيق قيد التتبع بالفعل</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">تسجيل الدخول إلى GitHub</string>\n    <string name=\"profile_sign_in_description\">افتح التجربة الكاملة. أدر تطبيقاتك، زامن تفضيلاتك، وتصفح بشكل أسرع.</string>\n    <string name=\"profile_repos\">المستودعات</string>\n    <string name=\"profile_login\">تسجيل الدخول</string>\n    <string name=\"profile_stars_description\">مستودعاتك المميزة بنجمة من GitHub</string>\n    <string name=\"profile_favourites_description\">مستودعاتك المفضلة المحفوظة محلياً</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">انتهت الجلسة</string>\n    <string name=\"session_expired_message\">انتهت جلسة GitHub الخاصة بك أو تم إلغاء الرمز المميز. يرجى تسجيل الدخول مرة أخرى لمتابعة استخدام الميزات المصادق عليها.</string>\n    <string name=\"session_expired_hint\">لا يزال بإمكانك التصفح كضيف مع طلبات API محدودة.</string>\n    <string name=\"sign_in_again\">تسجيل الدخول مرة أخرى</string>\n    <string name=\"continue_as_guest\">المتابعة كضيف</string>\n\n    <!-- Logout improvements -->\n    <string name=\"logout_revocation_note\">سيؤدي هذا إلى مسح جلستك المحلية والبيانات المخزنة مؤقتاً. لإلغاء الوصول بالكامل، قم بزيارة إعدادات GitHub > التطبيقات.</string>\n\n    <!-- Auth UI improvements -->\n    <string name=\"auth_code_expires_in\">ينتهي الرمز خلال %1$s</string>\n    <string name=\"auth_error_code_expired\">انتهت صلاحية رمز الجهاز.</string>\n    <string name=\"auth_hint_try_again\">يرجى محاولة تسجيل الدخول مرة أخرى للحصول على رمز جديد.</string>\n    <string name=\"auth_hint_check_connection\">يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.</string>\n    <string name=\"auth_hint_denied\">لقد رفضت طلب التفويض. حاول مرة أخرى إذا كان ذلك غير مقصود.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">اقرأ المزيد</string>\n    <string name=\"show_less\">عرض أقل</string>\n\n    <string name=\"share_repository\">مشاركة المستودع</string>\n    <string name=\"failed_to_share_link\">فشل مشاركة الرابط</string>\n    <string name=\"link_copied_to_clipboard\">تم نسخ الرابط إلى الحافظة</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">ترجمة</string>\n    <string name=\"translating\">جارٍ الترجمة…</string>\n    <string name=\"show_original\">عرض الأصلي</string>\n    <string name=\"translated_to\">تُرجم إلى %1$s</string>\n    <string name=\"translate_to\">ترجمة إلى…</string>\n    <string name=\"search_language\">البحث عن لغة</string>\n    <string name=\"change_language\">تغيير اللغة</string>\n    <string name=\"translation_failed\">فشلت الترجمة. يرجى المحاولة مرة أخرى.</string>\n\n    <!-- Search - GitHub Link -->\n    <string name=\"open_github_link\">فتح رابط GitHub</string>\n    <string name=\"clipboard_link_detected\">تم اكتشاف رابط GitHub في الحافظة</string>\n    <string name=\"auto_detect_clipboard_links\">الكشف التلقائي عن روابط الحافظة</string>\n    <string name=\"auto_detect_clipboard_description\">الكشف التلقائي عن روابط GitHub من الحافظة عند فتح البحث</string>\n    <string name=\"detected_links\">الروابط المكتشفة</string>\n    <string name=\"open_in_app\">فتح في التطبيق</string>\n    <string name=\"no_github_link_in_clipboard\">لم يتم العثور على رابط GitHub في الحافظة</string>\n\n    <string name=\"storage\">التخزين</string>\n    <string name=\"clear_cache\">مسح ذاكرة التخزين المؤقت</string>\n    <string name=\"current_size\">الحجم الحالي:</string>\n    <string name=\"clear\">مسح</string>\n\n    <string name=\"sponsor_title\">ادعم GitHub Store</string>\n    <string name=\"sponsor_button\">ادعم المشروع</string>\n    <string name=\"sponsor_hero_title\">صُنع بحب،\\nويستمر بالقهوة</string>\n\n    <string name=\"sponsor_hero_subtitle\">وصل GitHub Store إلى أكثر من 130,000 تنزيل و 7,700 نجمة على GitHub — مجاني 100٪، بدون إعلانات أو تتبع.</string>\n\n    <string name=\"sponsor_personal_note\">أقوم ببناء وصيانة هذا المشروع بالكامل بمفردي أثناء إنهاء دراستي الثانوية.</string>\n\n    <string name=\"sponsor_kodee_title\">صوّت لـ GitHub Store!</string>\n    <string name=\"sponsor_kodee_subtitle\">تم ترشيح GitHub Store لجوائز Golden Kodee في KotlinConf 2026.</string>\n\n    <string name=\"sponsor_kodee_register\">1. التسجيل</string>\n    <string name=\"sponsor_kodee_vote\">2. التصويت</string>\n\n    <string name=\"sponsor_kodee_deadline\">ينتهي التصويت في 22 مارس</string>\n\n    <string name=\"sponsor_kodee_step1\">1. سجل في منصة الجوائز (المتابعة عبر Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. اضغط على زر التصويت أدناه</string>\n    <string name=\"sponsor_kodee_step3\">3. ابحث عن Usmon Narzullayev واضغط تصويت</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">دعم متكرر أو لمرة واحدة عبر GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">دعم سريع لمرة واحدة</string>\n\n    <string name=\"sponsor_other_ways_title\">طرق أخرى للمساعدة</string>\n\n    <string name=\"sponsor_star_repo\">ضع نجمة للمستودع</string>\n    <string name=\"sponsor_star_repo_desc\">يساعد الآخرين على اكتشاف GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">الإبلاغ عن الأخطاء</string>\n    <string name=\"sponsor_report_bugs_desc\">يجعل التطبيق أفضل للجميع</string>\n\n    <string name=\"sponsor_share\">شارك مع الأصدقاء</string>\n    <string name=\"sponsor_share_desc\">انشر الخبر بين المطورين</string>\n\n    <string name=\"sponsor_thank_you\">كل دعم — مالي أو غير ذلك — يساعد في إبقاء هذا المشروع حيًا. شكرًا لك!</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">التثبيت</string>\n    <string name=\"installer_type_default\">الافتراضي</string>\n    <string name=\"installer_type_default_description\">نافذة التثبيت القياسية للنظام</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">تثبيت صامت بدون مطالبات</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku غير مثبت</string>\n    <string name=\"shizuku_status_not_running\">Shizuku لا يعمل</string>\n    <string name=\"shizuku_status_permission_needed\">يتطلب إذنًا</string>\n    <string name=\"shizuku_status_ready\">جاهز</string>\n    <string name=\"shizuku_grant_permission\">منح الإذن</string>\n    <string name=\"shizuku_install_hint\">ثبّت Shizuku لتفعيل التثبيت الصامت</string>\n    <string name=\"shizuku_start_hint\">شغّل Shizuku لتفعيل التثبيت الصامت</string>\n    <string name=\"shizuku_install_failed_fallback\">فشل تثبيت Shizuku، يتم استخدام المثبت القياسي</string>\n\n    <string name=\"auto_update_title\">تحديث التطبيقات تلقائيًا</string>\n    <string name=\"auto_update_description\">تنزيل التحديثات وتثبيتها تلقائيًا في الخلفية عبر Shizuku</string>\n\n    <string name=\"section_updates\">التحديثات</string>\n    <string name=\"update_check_interval_title\">فترة التحقق من التحديثات</string>\n    <string name=\"update_check_interval_description\">عدد مرات التحقق من تحديثات التطبيق في الخلفية</string>\n    <string name=\"interval_3h\">٣ ساعات</string>\n    <string name=\"interval_6h\">٦ ساعات</string>\n    <string name=\"interval_12h\">١٢ ساعة</string>\n    <string name=\"interval_24h\">٢٤ ساعة</string>\n\n    <string name=\"add_by_link\">إضافة عبر رابط</string>\n    <string name=\"link_app_title\">ربط التطبيق بالمستودع</string>\n    <string name=\"pick_installed_app\">اختر تطبيقاً مثبتاً لربطه بمستودع GitHub</string>\n    <string name=\"search_apps_hint\">البحث عن التطبيقات…</string>\n    <string name=\"enter_repo_url\">رابط مستودع GitHub</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">جارٍ التحقق…</string>\n    <string name=\"link_and_track\">ربط وتتبع</string>\n    <string name=\"checking_release\">التحقق من آخر إصدار…</string>\n    <string name=\"downloading_for_verification\">تنزيل APK للتحقق…</string>\n    <string name=\"verifying_signing_key\">التحقق من مفتاح التوقيع…</string>\n    <string name=\"package_name_mismatch\">عدم تطابق اسم الحزمة: ملف APK هو %1$s، لكن التطبيق المحدد هو %2$s</string>\n    <string name=\"signing_key_mismatch_link\">عدم تطابق مفتاح التوقيع: ملف APK في هذا المستودع موقّع من مطور مختلف</string>\n    <string name=\"select_asset_title\">اختر المثبّت</string>\n    <string name=\"select_asset_description\">اختر ملف APK للتحقق من مطابقته للتطبيق المثبت</string>\n    <string name=\"download_failed\">فشل التنزيل</string>\n    <string name=\"export_apps\">تصدير</string>\n    <string name=\"import_apps\">استيراد</string>\n    <string name=\"import_apps_title\">استيراد التطبيقات</string>\n    <string name=\"import_apps_description\">الصق ملف JSON المُصدَّر لاستعادة التطبيقات المتتبعة</string>\n    <string name=\"import_apps_hint\">الصق JSON المُصدَّر هنا…</string>\n    <string name=\"include_pre_releases_title\">تضمين الإصدارات التجريبية</string>\n    <string name=\"include_pre_releases_description\">تتبع الإصدارات التجريبية عند التحقق من التحديثات. عند التعطيل، يتم اعتبار الإصدارات المستقرة فقط.</string>\n    <string name=\"confirm_uninstall_title\">إلغاء تثبيت التطبيق؟</string>\n    <string name=\"confirm_uninstall_message\">هل أنت متأكد من إلغاء تثبيت %1$s؟ لا يمكن التراجع عن هذا الإجراء وقد تُفقد بيانات التطبيق.</string>\n    <string name=\"invalid_github_url\">رابط GitHub غير صالح. استخدم التنسيق: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">المستودع غير موجود: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">تم تجاوز حد طلبات GitHub API. حاول لاحقاً.</string>\n    <string name=\"failed_to_link\">فشل الربط: %1$s</string>\n    <string name=\"failed_to_load_apps\">فشل تحميل التطبيقات المثبتة</string>\n    <string name=\"app_linked_success\">تم ربط %1$s بـ %2$s/%3$s</string>\n    <string name=\"export_failed\">فشل التصدير: %1$s</string>\n    <string name=\"import_failed\">فشل الاستيراد: %1$s</string>\n    <string name=\"imported_apps_summary\">تم استيراد %1$d تطبيقات</string>\n    <string name=\"imported_skipped\">، %1$d تم تخطيها</string>\n    <string name=\"imported_failed\">، %1$d فشلت</string>\n    <string name=\"signing_key_changed_title\">تغيّر مفتاح التوقيع</string>\n    <string name=\"signing_key_changed_message\">تغيّرت شهادة توقيع هذا التطبيق منذ تثبيته لأول مرة.\\n\\nقد يعني هذا أن المطور غيّر مفتاح التوقيع، أو أن الملف قد تم التلاعب به.\\n\\nالمتوقع: %1$s\\nالمستلم: %2$s</string>\n    <string name=\"install_anyway\">التثبيت على أي حال</string>\n    <string name=\"verified_build\">بناء موثق</string>\n    <string name=\"checking_attestation\">جارٍ التحقق\\u2026</string>\n    <string name=\"assets_title\">الملفات</string>\n    <string name=\"no_assets_selected\">لا توجد ملفات</string>\n    <string name=\"no_assets_in_list\">لا توجد ملفات مرتبطة بهذا الإصدار</string>\n    <string name=\"assets_selection_label\">اختر خيار الملف</string>\n    <string name=\"multiple_assets_info_dialog_title\">ملفات متعددة متاحة</string>\n    <string name=\"multiple_assets_info_dialog_text\">تتوفر عدة ملفات قابلة للتثبيت لهذا الإصدار. يرجى مراجعة القائمة واختيار الملف المناسب لجهازك.</string>\n    <string name=\"icon_content_description_info\">معلومات</string>\n    <string name=\"translation_error_retry\">إعادة المحاولة</string>\n    <string name=\"translated_from\">اكتشاف تلقائي: %1$s</string>\n    <string name=\"select_language\">اختر اللغة</string>\n    <string name=\"update_package_mismatch\">عدم تطابق الحزمة: ملف APK هو %1$s، لكن التطبيق المثبت هو %2$s. تم حظر التحديث.</string>\n    <string name=\"update_signing_key_mismatch\">عدم تطابق مفتاح التوقيع: تم توقيع التحديث بواسطة مطور مختلف. تم حظر التحديث.</string>\n    <string name=\"liquid_glass_option_title\">تأثير الزجاج السائل</string>\n    <string name=\"liquid_glass_option_description\">تحسين الواجهة بمظهر زجاجي ناعم</string>\n\n    <string name=\"hide_seen_title\">إخفاء المستودعات المشاهَدة</string>\n    <string name=\"hide_seen_description\">إخفاء المستودعات التي شاهدتها بالفعل من خلاصات الاكتشاف</string>\n    <string name=\"clear_seen_history\">مسح سجل المشاهدة</string>\n    <string name=\"clear_seen_history_description\">إعادة تعيين جميع المستودعات المشاهَدة لتظهر مجدداً في الخلاصات</string>\n    <string name=\"seen_history_cleared\">تم مسح سجل المشاهدة</string>\n    <string name=\"seen_badge\">تمت المشاهدة</string>\n</resources>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">ইনস্টল করা অ্যাপসমূহ</string>\n    <string name=\"navigate_back\">পেছনে যান</string>\n    <string name=\"check_for_updates\">আপডেট পরীক্ষা করুন</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">%1$s চালু করা যায়নি</string>\n    <string name=\"failed_to_open\">%1$s খুলতে ব্যর্থ</string>\n    <string name=\"failed_to_update\">%1$s আপডেট করতে ব্যর্থ: %2$s</string>\n    <string name=\"update_failed\">আপডেট ব্যর্থ</string>\n    <string name=\"update_all_failed\">সব আপডেট ব্যর্থ হয়েছে: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">সব অ্যাপ সফলভাবে আপডেট হয়েছে</string>\n    <string name=\"no_updates_available\">কোনো আপডেট পাওয়া যায়নি</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">আপনার অ্যাপ খুঁজুন</string>\n    <string name=\"no_apps_found\">কোনো অ্যাপ পাওয়া যায়নি</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">সব আপডেট করুন</string>\n    <string name=\"update\">আপডেট</string>\n    <string name=\"open\">খুলুন</string>\n    <string name=\"cancel\">বাতিল</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">পরীক্ষা করা হচ্ছে…</string>\n    <string name=\"updated_successfully\">সফলভাবে আপডেট হয়েছে</string>\n    <string name=\"error_with_message\">ত্রুটি: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">%2$d এর মধ্যে %1$d টি আপডেট করা হচ্ছে</string>\n    <string name=\"currently_updating\">বর্তমানে: %1$s</string>\n\n    \n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">অনুমোদনের জন্য অপেক্ষা করা হচ্ছে…</string>\n    <string name=\"signed_in\">সাইন ইন সম্পন্ন!</string>\n    <string name=\"redirecting_message\">আপনি এখন অ্যাপটি ব্যবহার করতে পারেন। রিডাইরেক্ট করা হচ্ছে…</string>\n    <string name=\"try_again\">আবার চেষ্টা করুন</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">ত্রুটি: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">GitHub-এ এই কোডটি লিখুন:</string>\n    <string name=\"copy_code\">কোড কপি করুন</string>\n    <string name=\"open_github\">GitHub খুলুন</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">সম্পূর্ণ অভিজ্ঞতা\\nআনলক করুন</string>\n\n    <string name=\"more_requests\">আরও অনুরোধ</string>\n    <string name=\"more_requests_description\">\n        বেশি API রেট লিমিট এবং বিঘ্ন থেকে মুক্তি পেতে সাইন ইন করুন।\n    </string>\n\n    <string name=\"sign_in_with_github\">GitHub দিয়ে সাইন ইন করুন</string>\n\n    <string name=\"error_cancelled\">বাতিল করা হয়েছে</string>\n    <string name=\"error_unknown\">অজানা ত্রুটি</string>\n\n    <string name=\"language_label\">ভাষা:</string>\n\n    <string name=\"discover_repositories\">রিপোজিটরি আবিষ্কার করুন</string>\n    <string name=\"search_repositories_hint\">রিপো, বিবরণ খুঁজুন…</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">ভাষা অনুযায়ী ফিল্টার করুন</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">%1$d টি ফলাফল পাওয়া গেছে</string>\n\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">সাজান</string>\n    <string name=\"close\">বন্ধ করুন</string>\n\n    <string name=\"sort_most_stars\">সবচেয়ে বেশি স্টার</string>\n    <string name=\"sort_most_forks\">সবচেয়ে বেশি ফর্ক</string>\n    <string name=\"sort_best_match\">সেরা মিল</string>\n\n    <string name=\"sort_order_descending\">অধোগামী</string>\n    <string name=\"sort_order_ascending\">ঊর্ধ্বগামী</string>\n    <string name=\"sort_label\">সাজান</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">সব প্রোগ্রামিং ভাষা</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">অনুসন্ধান ব্যর্থ হয়েছে</string>\n    <string name=\"no_repositories_found\">কোনো রিপোজিটরি পাওয়া যায়নি</string>\n\n    <!-- Settings -->\n    <string name=\"profile_title\">প্রোফাইল</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">চেহারা</string>\n    <string name=\"section_about\">সম্পর্কে</string>\n    <string name=\"section_network\">নেটওয়ার্ক</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">থিমের রঙ</string>\n    <string name=\"amoled_black_theme\">AMOLED কালো থিম</string>\n    <string name=\"amoled_black_description\">অন্ধকার মোডের জন্য সম্পূর্ণ কালো ব্যাকগ্রাউন্ড</string>\n    <string name=\"selected_color\">নির্বাচিত রঙ: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">সংস্করণ</string>\n    <string name=\"help_support\">সহায়তা ও সাপোর্ট</string>\n\n    <!-- Account -->\n    <string name=\"logout\">লগআউট</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">সফলভাবে লগআউট হয়েছে, রিডাইরেক্ট করা হচ্ছে...</string>\n    <string name=\"cache_cleared\">ক্যাশ সফলভাবে পরিষ্কার করা হয়েছে</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">সতর্কতা!</string>\n    <string name=\"logout_confirmation\">আপনি কি নিশ্চিতভাবে লগআউট করতে চান?</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">ডাইনামিক</string>\n    <string name=\"theme_ocean\">সমুদ্র</string>\n    <string name=\"theme_purple\">বেগুনি</string>\n    <string name=\"theme_forest\">বন</string>\n    <string name=\"theme_slate\">স্লেট</string>\n    <string name=\"theme_amber\">অ্যাম্বার</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">রিপোজিটরি খুলুন</string>\n    <string name=\"open_in_browser\">ব্রাউজারে খুলুন</string>\n    <string name=\"cancel_download\">ডাউনলোড বাতিল করুন</string>\n    <string name=\"show_install_options\">ইনস্টল অপশন দেখান</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">বিস্তারিত লোড করতে ত্রুটি দেখা গেছে</string>\n    <string name=\"retry\">পুনরায় চেষ্টা করুন</string>\n    <string name=\"no_description\">কোনো বিবরণ দেওয়া হয়নি।</string>\n    <string name=\"no_release_notes\">কোনো রিলিজ নোট নেই।</string>\n    <string name=\"report_issue\">সমস্যা রিপোর্ট করুন</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">এই অ্যাপ বৃত্তান্ত</string>\n    <string name=\"install_logs\">ইনস্টল লগ</string>\n    <string name=\"author\">লেখক</string>\n    <string name=\"whats_new\">নতুন কী আছে</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">ইনস্টল করা</string>\n    <string name=\"update_available\">আপডেট উপলব্ধ</string>\n    <string name=\"not_available\">উপলব্ধ নয়</string>\n    <string name=\"install_latest\">সর্বশেষটি ইনস্টল করুন</string>\n    <string name=\"reinstall\">পুনরায় ইনস্টল করুন</string>\n    <string name=\"update_app\">অ্যাপ আপডেট করুন</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">ডাউনলোড হচ্ছে</string>\n    <string name=\"updating\">আপডেট হচ্ছে</string>\n    <string name=\"verifying\">যাচাই করা হচ্ছে</string>\n    <string name=\"installing\">ইনস্টল হচ্ছে</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Obtainium-এ খুলুন</string>\n    <string name=\"obtainium_description\">স্বয়ংক্রিয়ভাবে আপডেট পরিচালনা করুন</string>\n    <string name=\"inspect_with_appmanager\">AppManager দিয়ে পরীক্ষা করুন</string>\n    <string name=\"appmanager_description\">অনুমতি, ট্র্যাকার ও নিরাপত্তা যাচাই করুন</string>\n\n    <!-- Author -->\n    <string name=\"profile\">প্রোফাইল</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">ফর্ক</string>\n    <string name=\"stars\">স্টার</string>\n    <string name=\"issues\">ইস্যু</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">%1$s দ্বারা</string>\n    <string name=\"installed_version\">• ইনস্টল করা: %1$s</string>\n    <string name=\"architecture_compatible\">আর্কিটেকচার উপযোগী</string>\n    <string name=\"update_to_version\">%1$s -এ আপডেট করুন</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">বিস্তারিত লোড করতে ব্যর্থ</string>\n    <string name=\"installer_saved_downloads\">ইনস্টলারটি Downloads ফোল্ডারে সংরক্ষিত হয়েছে</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">ডাউনলোড শুরু হয়েছে</string>\n    <string name=\"log_downloaded\">ডাউনলোড সম্পন্ন</string>\n    <string name=\"log_update_started\">আপডেট শুরু হয়েছে</string>\n    <string name=\"log_installed\">ইনস্টল হয়েছে</string>\n    <string name=\"log_updated\">আপডেট হয়েছে</string>\n    <string name=\"log_cancelled\">বাতিল হয়েছে</string>\n    <string name=\"log_install_started\">ইনস্টলেশন শুরু হয়েছে</string>\n    <string name=\"log_error\">ত্রুটি</string>\n    <string name=\"log_error_with_message\">ত্রুটি: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">AppManager-এর জন্য প্রস্তুত করা হচ্ছে</string>\n    <string name=\"log_opened_appmanager\">AppManager-এ খোলা হয়েছে</string>\n    <string name=\"log_permission_blocked\">ডিভাইস নীতি দ্বারা ইনস্টল অনুমতি অবরুদ্ধ</string>\n    <string name=\"log_opened_external_installer\">বাহ্যিক ইনস্টলারে খোলা হয়েছে</string>\n    <string name=\"install_permission_unavailable\">ইনস্টল অনুমতি অনুপলব্ধ</string>\n    <string name=\"install_permission_blocked_message\">APK সফলভাবে ডাউনলোড হয়েছে কিন্তু এই ডিভাইসে সরাসরি ইনস্টল করার অনুমতি নেই। আপনি কি এটি বাহ্যিক ইনস্টলার দিয়ে খুলতে চান?</string>\n    <string name=\"open_with_external_installer\">বাহ্যিক ইনস্টলার দিয়ে খুলুন</string>\n    <string name=\"external_installer_description\">APK ইনস্টল করতে তৃতীয় পক্ষের অ্যাপ ব্যবহার করুন</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">ত্রুটি: %1$s</string>\n    <string name=\"error_asset_not_supported\">.%1$s অ্যাসেট টাইপ সমর্থিত নয়</string>\n    <string name=\"error_file_not_found\">ডাউনলোড করা ফাইল পাওয়া যায়নি</string>\n\n    <string name=\"home_category_trending\">ট্রেন্ডিং</string>\n    <string name=\"home_category_hot_release\">হট রিলিজ</string>\n    <string name=\"home_category_most_popular\">সবচেয়ে জনপ্রিয়</string>\n\n    <string name=\"home_finding_repositories\">রিপোজিটরি খোঁজা হচ্ছে...</string>\n    <string name=\"home_loading_more\">আরও লোড হচ্ছে...</string>\n    <string name=\"home_no_more_repositories\">আর কোনো রিপোজিটরি নেই</string>\n    <string name=\"home_retry\">পুনরায় চেষ্টা</string>\n    <string name=\"home_failed_to_load_repositories\">রিপোজিটরি লোড করতে ব্যর্থ</string>\n    <string name=\"home_view_details\">বিস্তারিত দেখুন</string>\n\n    <string name=\"updated_just_now\">এইমাত্র আপডেট হয়েছে</string>\n    <string name=\"updated_hours_ago\">%1$d ঘণ্টা আগে আপডেট হয়েছে</string>\n    <string name=\"updated_yesterday\">গতকাল আপডেট হয়েছে</string>\n    <string name=\"updated_days_ago\">%1$d দিন আগে আপডেট হয়েছে</string>\n    <string name=\"updated_on_date\">%1$s তারিখে আপডেট হয়েছে</string>\n\n    <string name=\"rate_limit_exceeded\">রেট লিমিট অতিক্রম করেছে</string>\n    <string name=\"rate_limit_used_all\">আপনি সব %1$d টি API অনুরোধ ব্যবহার করেছেন।</string>\n    <string name=\"rate_limit_used_all_free\">আপনি সব %1$d টি ফ্রি API অনুরোধ ব্যবহার করেছেন।</string>\n    <string name=\"rate_limit_resets_in_minutes\">%1$d মিনিটে রিসেট হবে</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 ৬০ এর বদলে প্রতি ঘণ্টায় ৫,০০০ অনুরোধ পেতে সাইন ইন করুন!</string>\n    <string name=\"rate_limit_sign_in\">সাইন ইন</string>\n    <string name=\"rate_limit_ok\">ঠিক আছে</string>\n    <string name=\"rate_limit_close\">বন্ধ করুন</string>\n\n    <string name=\"system_font\">সিস্টেম ফন্ট</string>\n    <string name=\"system_font_description\">আরও ভালো পাঠযোগ্যতার জন্য ডিভাইসের ফন্ট ব্যবহার করুন</string>\n\n    <string name=\"theme_light\">উজ্জ্বল</string>\n    <string name=\"theme_dark\">অন্ধকার</string>\n    <string name=\"theme_system\">সিস্টেম</string>\n\n    <string name=\"added_to_favourites\">রিপোজিটরি প্রিয় তালিকায় যোগ করা হয়েছে</string>\n    <string name=\"removed_from_favourites\">রিপোজিটরি প্রিয় তালিকা থেকে সরানো হয়েছে</string>\n    <string name=\"add_to_favourites\">প্রিয় তালিকায় যোগ করুন</string>\n    <string name=\"remove_from_favourites\">প্রিয় তালিকা থেকে সরান</string>\n    <string name=\"favourites\">প্রিয়</string>\n\n    <string name=\"added_just_now\">এইমাত্র যোগ করা হয়েছে</string>\n    <string name=\"added_hours_ago\">%1$d ঘণ্টা আগে যোগ করা হয়েছে</string>\n    <string name=\"added_yesterday\">গতকাল যোগ করা হয়েছে</string>\n    <string name=\"added_days_ago\">%1$d দিন আগে যোগ করা হয়েছে</string>\n    <string name=\"added_on_date\">%1$s তারিখে যোগ করা হয়েছে</string>\n\n    <string name=\"starred_repositories\">স্টার করা রিপোজিটরি</string>\n    <string name=\"repository_starred\">রিপোজিটরি স্টার করা হয়েছে</string>\n    <string name=\"repository_not_starred\">রিপোজিটরি স্টার করা হয়নি</string>\n    <string name=\"star_from_github\">আপনি GitHub থেকে রিপোজিটরি স্টার করতে পারেন</string>\n    <string name=\"unstar_from_github\">আপনি GitHub থেকে রিপোজিটরির স্টার সরাতে পারেন</string>\n\n    <string name=\"sign_in_required\">সাইন ইন প্রয়োজন</string>\n    <string name=\"sign_in_with_github_for_stars\">স্টার করা রিপোজিটরি দেখতে GitHub দিয়ে সাইন ইন করুন</string>\n    <string name=\"no_starred_repos\">কোনো স্টার করা রিপোজিটরি নেই</string>\n    <string name=\"star_repos_hint\">ইনস্টলযোগ্য রিলিজ থাকা রিপোজিটরি GitHub-এ স্টার করুন</string>\n    <string name=\"last_synced\">শেষ সিঙ্ক</string>\n    <string name=\"just_now\">এইমাত্র</string>\n    <string name=\"minutes_ago\">%1$d মিনিট আগে</string>\n    <string name=\"hours_ago\">%1$d ঘণ্টা আগে</string>\n    <string name=\"days_ago\">%1$d দিন আগে</string>\n    <string name=\"dismiss\">বন্ধ করুন</string>\n    <string name=\"sync_starred_failed\">স্টার করা রিপোজিটরি সিঙ্ক করতে ব্যর্থ হয়েছে</string>\n\n    <string name=\"developer_profile_title\">ডেভেলপার প্রোফাইল</string>\n    <string name=\"open_developer_profile\">ডেভেলপার প্রোফাইল খুলুন</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">রিপোজিটরি লোড করতে ব্যর্থ</string>\n    <string name=\"failed_to_load_profile\">প্রোফাইল লোড করতে ব্যর্থ</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">রিপোজিটরি</string>\n    <string name=\"followers\">অনুসরণকারী</string>\n    <string name=\"following\">অনুসরণ</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">রিপোজিটরি খুঁজুন…</string>\n    <string name=\"clear_search\">অনুসন্ধান মুছুন</string>\n    <string name=\"filter_all\">সব</string>\n    <string name=\"filter_with_releases\">রিলিজ সহ</string>\n    <string name=\"filter_installed\">ইনস্টল করা</string>\n    <string name=\"filter_favorites\">পছন্দ</string>\n    <string name=\"sort\">সাজান</string>\n    <string name=\"sort_recently_updated\">সাম্প্রতিক আপডেট</string>\n    <string name=\"sort_name\">নাম</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">রিপোজিটরি</string>\n    <string name=\"repositories_plural\">রিপোজিটরি</string>\n    <string name=\"showing_x_of_y_repositories\">%2$d টির মধ্যে %1$d টি রিপোজিটরি দেখানো হচ্ছে</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">ইনস্টল করার যোগ্য রিলিজ সহ কোনো রিপোজিটরি নেই</string>\n    <string name=\"no_installed_repos\">কোনো রিপোজিটরি ইনস্টল করা হয়নি</string>\n    <string name=\"no_favorite_repos\">কোনো পছন্দের রিপোজিটরি নেই</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">%1$s আপডেট করা হয়েছে</string>\n    <string name=\"has_release\">রিলিজ আছে</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d বছর আগে</string>\n    <string name=\"time_months_ago\">%1$d মাস আগে</string>\n    <string name=\"time_days_ago\">%1$d দিন আগে</string>\n    <string name=\"time_hours_ago\">%1$d ঘণ্টা আগে</string>\n    <string name=\"time_minutes_ago\">%1$d মিনিট আগে</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">এইমাত্র প্রকাশিত</string>\n    <string name=\"released_hours_ago\">%1$d ঘণ্টা আগে প্রকাশিত</string>\n    <string name=\"released_yesterday\">গতকাল প্রকাশিত</string>\n    <string name=\"released_days_ago\">%1$d দিন আগে প্রকাশিত</string>\n    <string name=\"released_on_date\">%1$s তারিখে প্রকাশিত</string>\n\n    <string name=\"bottom_nav_home_title\">হোম</string>\n    <string name=\"bottom_nav_search_title\">অনুসন্ধান</string>\n    <string name=\"bottom_nav_apps_title\">অ্যাপস</string>\n    <string name=\"bottom_nav_profile_title\">প্রোফাইল</string>\n\n    <string name=\"forked_repository\">ফর্ক</string>\n\n    <string name=\"category_stable\">স্থিতিশীল</string>\n    <string name=\"category_pre_release\">প্রি-রিলিজ</string>\n    <string name=\"category_all\">সব</string>\n    <string name=\"select_version\">ভার্সন নির্বাচন করুন</string>\n    <string name=\"pre_release_badge\">প্রি-রিলিজ</string>\n    <string name=\"no_version_selected\">কোনো ভার্সন নির্বাচিত নয়</string>\n    <string name=\"versions_title\">ভার্সনসমূহ</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">ইনস্টল মুলতুবি</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">আনইনস্টল</string>\n    <string name=\"open_app\">খুলুন</string>\n    <string name=\"downgrade_requires_uninstall\">ডাউনগ্রেডের জন্য আনইনস্টল প্রয়োজন</string>\n    <string name=\"downgrade_warning_message\">সংস্করণ %1$s ইনস্টল করতে বর্তমান সংস্করণ (%2$s) প্রথমে আনইনস্টল করতে হবে। অ্যাপের ডেটা মুছে যাবে।</string>\n    <string name=\"uninstall_first\">প্রথমে আনইনস্টল করুন</string>\n    <string name=\"install_version\">%1$s ইনস্টল করুন</string>\n    <string name=\"failed_to_open_app\">%1$s খুলতে ব্যর্থ</string>\n    <string name=\"failed_to_uninstall\">%1$s আনইনস্টল করতে ব্যর্থ</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">সর্বশেষ</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">সর্বশেষ পরীক্ষা: %1$s</string>\n    <string name=\"last_checked_never\">কখনো পরীক্ষা করা হয়নি</string>\n    <string name=\"last_checked_just_now\">এইমাত্র</string>\n    <string name=\"last_checked_minutes_ago\">%1$d মিনিট আগে</string>\n    <string name=\"last_checked_hours_ago\">%1$d ঘণ্টা আগে</string>\n    <string name=\"checking_for_updates\">আপডেট পরীক্ষা করা হচ্ছে…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">প্রক্সি ধরন</string>\n    <string name=\"proxy_none\">নেই</string>\n    <string name=\"proxy_system\">সিস্টেম</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">হোস্ট</string>\n    <string name=\"proxy_port\">পোর্ট</string>\n    <string name=\"proxy_username\">ব্যবহারকারীর নাম (ঐচ্ছিক)</string>\n    <string name=\"proxy_password\">পাসওয়ার্ড (ঐচ্ছিক)</string>\n    <string name=\"proxy_save\">প্রক্সি সংরক্ষণ</string>\n    <string name=\"proxy_saved\">প্রক্সি সেটিংস সংরক্ষিত হয়েছে</string>\n    <string name=\"proxy_system_description\">আপনার ডিভাইসের প্রক্সি সেটিংস ব্যবহার করে</string>\n    <string name=\"proxy_port_error\">পোর্ট ১–৬৫৫৩৫ এর মধ্যে হতে হবে</string>\n    <string name=\"proxy_none_description\">সরাসরি সংযোগ, কোনো প্রক্সি নেই</string>\n    <string name=\"failed_to_save_proxy_settings\">প্রক্সি সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে</string>\n    <string name=\"proxy_host_required\">প্রক্সি হোস্ট প্রয়োজন</string>\n    <string name=\"invalid_proxy_port\">অবৈধ প্রক্সি পোর্ট</string>\n    <string name=\"proxy_show_password\">পাসওয়ার্ড দেখান</string>\n    <string name=\"proxy_hide_password\">পাসওয়ার্ড লুকান</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">এই অ্যাপ ট্র্যাক করুন</string>\n    <string name=\"app_tracked_successfully\">অ্যাপ ট্র্যাকিং তালিকায় যোগ করা হয়েছে</string>\n    <string name=\"failed_to_track_app\">অ্যাপ ট্র্যাক করতে ব্যর্থ: %1$s</string>\n    <string name=\"already_tracked\">অ্যাপটি ইতিমধ্যে ট্র্যাক করা হচ্ছে</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">GitHub-এ সাইন ইন করুন</string>\n    <string name=\"profile_sign_in_description\">সম্পূর্ণ অভিজ্ঞতা আনলক করুন। আপনার অ্যাপ পরিচালনা করুন, পছন্দ সিঙ্ক করুন এবং দ্রুত ব্রাউজ করুন।</string>\n    <string name=\"profile_repos\">রিপোজিটরি</string>\n    <string name=\"profile_login\">লগইন</string>\n    <string name=\"profile_stars_description\">GitHub-এ আপনার স্টার করা রিপোজিটরি</string>\n    <string name=\"profile_favourites_description\">স্থানীয়ভাবে সংরক্ষিত আপনার প্রিয় রিপোজিটরি</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">সেশনের মেয়াদ শেষ</string>\n    <string name=\"session_expired_message\">আপনার GitHub সেশনের মেয়াদ শেষ হয়ে গেছে বা টোকেনটি প্রত্যাহার করা হয়েছে। প্রমাণিত বৈশিষ্ট্যগুলি ব্যবহার চালিয়ে যেতে অনুগ্রহ করে আবার সাইন ইন করুন।</string>\n    <string name=\"session_expired_hint\">আপনি সীমিত API অনুরোধ সহ অতিথি হিসেবে ব্রাউজ করতে পারেন।</string>\n    <string name=\"sign_in_again\">আবার সাইন ইন করুন</string>\n    <string name=\"continue_as_guest\">অতিথি হিসেবে চালিয়ে যান</string>\n    <string name=\"logout_revocation_note\">এটি আপনার স্থানীয় সেশন এবং ক্যাশ ডেটা মুছে ফেলবে। সম্পূর্ণরূপে অ্যাক্সেস প্রত্যাহার করতে, GitHub Settings > Applications এ যান।</string>\n    <string name=\"auth_code_expires_in\">কোডের মেয়াদ শেষ হবে %1$s এ</string>\n    <string name=\"auth_error_code_expired\">ডিভাইস কোডের মেয়াদ শেষ হয়ে গেছে।</string>\n    <string name=\"auth_hint_try_again\">একটি নতুন কোড পেতে অনুগ্রহ করে আবার সাইন ইন করার চেষ্টা করুন।</string>\n    <string name=\"auth_hint_check_connection\">অনুগ্রহ করে আপনার ইন্টারনেট সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।</string>\n    <string name=\"auth_hint_denied\">আপনি অনুমোদনের অনুরোধ প্রত্যাখ্যান করেছেন। এটি অনিচ্ছাকৃত হলে আবার চেষ্টা করুন।</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">আরও পড়ুন</string>\n    <string name=\"show_less\">কম দেখান</string>\n\n    <string name=\"share_repository\">রিপোজিটরি শেয়ার করুন</string>\n    <string name=\"failed_to_share_link\">লিংক শেয়ার করতে ব্যর্থ হয়েছে</string>\n    <string name=\"link_copied_to_clipboard\">লিংক ক্লিপবোর্ডে কপি করা হয়েছে</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">অনুবাদ করুন</string>\n    <string name=\"translating\">অনুবাদ হচ্ছে…</string>\n    <string name=\"show_original\">মূল দেখান</string>\n    <string name=\"translated_to\">%1$s এ অনুবাদিত</string>\n    <string name=\"translate_to\">অনুবাদ করুন…</string>\n    <string name=\"search_language\">ভাষা খুঁজুন</string>\n    <string name=\"change_language\">ভাষা পরিবর্তন করুন</string>\n    <string name=\"translation_failed\">অনুবাদ ব্যর্থ হয়েছে। আবার চেষ্টা করুন।</string>\n\n    <string name=\"open_github_link\">GitHub লিংক খুলুন</string>\n    <string name=\"clipboard_link_detected\">ক্লিপবোর্ডে GitHub লিংক পাওয়া গেছে</string>\n    <string name=\"auto_detect_clipboard_links\">ক্লিপবোর্ড লিংক স্বয়ংক্রিয় সনাক্তকরণ</string>\n    <string name=\"auto_detect_clipboard_description\">অনুসন্ধান খোলার সময় স্বয়ংক্রিয়ভাবে ক্লিপবোর্ড থেকে GitHub লিংক সনাক্ত করুন</string>\n    <string name=\"detected_links\">সনাক্তকৃত লিংক</string>\n    <string name=\"open_in_app\">অ্যাপে খুলুন</string>\n    <string name=\"no_github_link_in_clipboard\">ক্লিপবোর্ডে কোনো GitHub লিংক পাওয়া যায়নি</string>\n\n    <string name=\"storage\">স্টোরেজ</string>\n    <string name=\"clear_cache\">ক্যাশে পরিষ্কার করুন</string>\n    <string name=\"current_size\">বর্তমান আকার:</string>\n    <string name=\"clear\">পরিষ্কার করুন</string>\n\n    <string name=\"sponsor_title\">GitHub Store সমর্থন করুন</string>\n    <string name=\"sponsor_button\">প্রকল্পকে সমর্থন করুন</string>\n    <string name=\"sponsor_hero_title\">ভালোবাসা দিয়ে তৈরি,\\nকফি দিয়ে চালিত</string>\n\n    <string name=\"sponsor_hero_subtitle\">GitHub Store 130,000+ ডাউনলোড এবং 7,700+ GitHub স্টার অর্জন করেছে — ১০০% ফ্রি, কোনো বিজ্ঞাপন নেই, কোনো ট্র্যাকিং নেই।</string>\n\n    <string name=\"sponsor_personal_note\">আমি উচ্চ বিদ্যালয় শেষ করার সময় একাই এই প্রকল্পটি তৈরি ও রক্ষণাবেক্ষণ করছি। আপনার সমর্থন — ছোট হলেও — অ্যাপটিকে বাগমুক্ত রাখতে, অবকাঠামো খরচ চালাতে এবং নতুন ফিচার আনতে সাহায্য করে।</string>\n\n    <string name=\"sponsor_kodee_title\">GitHub Store এর জন্য ভোট দিন!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store KotlinConf 2026 এর Golden Kodee Awards এর জন্য মনোনীত হয়েছে।</string>\n\n    <string name=\"sponsor_kodee_register\">1. নিবন্ধন করুন</string>\n    <string name=\"sponsor_kodee_vote\">2. ভোট দিন</string>\n\n    <string name=\"sponsor_kodee_deadline\">ভোট ২২ মার্চ পর্যন্ত</string>\n\n    <string name=\"sponsor_kodee_step1\">1. পুরস্কার প্ল্যাটফর্মে নিবন্ধন করুন (Google দিয়ে চালিয়ে যান)</string>\n    <string name=\"sponsor_kodee_step2\">2. নিচে Vote চাপুন</string>\n    <string name=\"sponsor_kodee_step3\">3. Usmon Narzullayev খুঁজে Vote চাপুন</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">GitHub এর মাধ্যমে একবার বা নিয়মিত সমর্থন</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">দ্রুত একবারের সমর্থন</string>\n\n    <string name=\"sponsor_other_ways_title\">সহায়তার অন্যান্য উপায়</string>\n\n    <string name=\"sponsor_star_repo\">রিপোজিটরিতে স্টার দিন</string>\n    <string name=\"sponsor_star_repo_desc\">অন্যদের GitHub Store খুঁজে পেতে সাহায্য করে</string>\n\n    <string name=\"sponsor_report_bugs\">বাগ রিপোর্ট করুন</string>\n    <string name=\"sponsor_report_bugs_desc\">অ্যাপটিকে আরও ভালো করে</string>\n\n    <string name=\"sponsor_share\">বন্ধুদের সাথে শেয়ার করুন</string>\n    <string name=\"sponsor_share_desc\">অন্যান্য ডেভেলপারদের জানাতে সাহায্য করে</string>\n\n    <string name=\"sponsor_thank_you\">যে কোনো সমর্থন — অর্থনৈতিক হোক বা না হোক — এই প্রকল্পটিকে বাঁচিয়ে রাখে। ধন্যবাদ!</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">ইনস্টলেশন</string>\n    <string name=\"installer_type_default\">ডিফল্ট</string>\n    <string name=\"installer_type_default_description\">স্ট্যান্ডার্ড সিস্টেম ইনস্টল ডায়ালগ</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">প্রম্পট ছাড়া নীরব ইনস্টল</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku ইনস্টল করা নেই</string>\n    <string name=\"shizuku_status_not_running\">Shizuku চলছে না</string>\n    <string name=\"shizuku_status_permission_needed\">অনুমতি প্রয়োজন</string>\n    <string name=\"shizuku_status_ready\">প্রস্তুত</string>\n    <string name=\"shizuku_grant_permission\">অনুমতি দিন</string>\n    <string name=\"shizuku_install_hint\">নীরব ইনস্টল সক্রিয় করতে Shizuku ইনস্টল করুন</string>\n    <string name=\"shizuku_start_hint\">নীরব ইনস্টল সক্রিয় করতে Shizuku চালু করুন</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizuku ইনস্টল ব্যর্থ, স্ট্যান্ডার্ড ইনস্টলার ব্যবহার করা হচ্ছে</string>\n\n    <string name=\"auto_update_title\">স্বয়ংক্রিয়ভাবে অ্যাপ আপডেট করুন</string>\n    <string name=\"auto_update_description\">Shizuku এর মাধ্যমে ব্যাকগ্রাউন্ডে স্বয়ংক্রিয়ভাবে আপডেট ডাউনলোড এবং ইনস্টল করুন</string>\n\n    <string name=\"section_updates\">আপডেট</string>\n    <string name=\"update_check_interval_title\">আপডেট চেক করার ব্যবধান</string>\n    <string name=\"update_check_interval_description\">ব্যাকগ্রাউন্ডে কতক্ষণ পর পর অ্যাপ আপডেট খোঁজা হবে</string>\n    <string name=\"interval_3h\">৩ঘ</string>\n    <string name=\"interval_6h\">৬ঘ</string>\n    <string name=\"interval_12h\">১২ঘ</string>\n    <string name=\"interval_24h\">২৪ঘ</string>\n\n    <string name=\"add_by_link\">লিঙ্ক দিয়ে যোগ করুন</string>\n    <string name=\"link_app_title\">অ্যাপ রিপোজিটরিতে লিঙ্ক করুন</string>\n    <string name=\"pick_installed_app\">GitHub রিপোজিটরিতে লিঙ্ক করতে একটি ইনস্টল করা অ্যাপ বেছে নিন</string>\n    <string name=\"search_apps_hint\">অ্যাপ খুঁজুন…</string>\n    <string name=\"enter_repo_url\">GitHub রিপোজিটরি URL</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">যাচাই হচ্ছে…</string>\n    <string name=\"link_and_track\">লিঙ্ক এবং ট্র্যাক করুন</string>\n    <string name=\"checking_release\">সর্বশেষ রিলিজ পরীক্ষা হচ্ছে…</string>\n    <string name=\"downloading_for_verification\">যাচাইয়ের জন্য APK ডাউনলোড হচ্ছে…</string>\n    <string name=\"verifying_signing_key\">সাইনিং কী যাচাই হচ্ছে…</string>\n    <string name=\"package_name_mismatch\">প্যাকেজ নাম মেলেনি: APK হলো %1$s, কিন্তু নির্বাচিত অ্যাপ হলো %2$s</string>\n    <string name=\"signing_key_mismatch_link\">সাইনিং কী মেলেনি: এই রিপোজিটরির APK একজন ভিন্ন ডেভেলপার দ্বারা স্বাক্ষরিত</string>\n    <string name=\"select_asset_title\">ইনস্টলার নির্বাচন করুন</string>\n    <string name=\"select_asset_description\">আপনার ইনস্টল করা অ্যাপের সাথে যাচাই করতে APK নির্বাচন করুন</string>\n    <string name=\"download_failed\">ডাউনলোড ব্যর্থ</string>\n    <string name=\"export_apps\">রপ্তানি</string>\n    <string name=\"import_apps\">আমদানি</string>\n    <string name=\"import_apps_title\">অ্যাপ আমদানি করুন</string>\n    <string name=\"import_apps_description\">ট্র্যাক করা অ্যাপ পুনরুদ্ধার করতে রপ্তানি করা JSON পেস্ট করুন</string>\n    <string name=\"import_apps_hint\">রপ্তানি করা JSON এখানে পেস্ট করুন…</string>\n    <string name=\"include_pre_releases_title\">প্রি-রিলিজ অন্তর্ভুক্ত করুন</string>\n    <string name=\"include_pre_releases_description\">আপডেট পরীক্ষার সময় প্রি-রিলিজ সংস্করণ ট্র্যাক করুন। নিষ্ক্রিয় থাকলে, শুধুমাত্র স্থিতিশীল রিলিজ বিবেচনা করা হয়।</string>\n    <string name=\"confirm_uninstall_title\">অ্যাপ আনইনস্টল করবেন?</string>\n    <string name=\"confirm_uninstall_message\">আপনি কি নিশ্চিত যে %1$s আনইনস্টল করতে চান? এই ক্রিয়া পূর্বাবস্থায় ফেরানো যাবে না এবং অ্যাপের ডেটা হারিয়ে যেতে পারে।</string>\n    <string name=\"invalid_github_url\">অবৈধ GitHub URL। ফর্ম্যাট ব্যবহার করুন: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">রিপোজিটরি পাওয়া যায়নি: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub API হার সীমা অতিক্রম করেছে। পরে আবার চেষ্টা করুন।</string>\n    <string name=\"failed_to_link\">লিঙ্ক করতে ব্যর্থ: %1$s</string>\n    <string name=\"failed_to_load_apps\">ইনস্টল করা অ্যাপ লোড করতে ব্যর্থ</string>\n    <string name=\"app_linked_success\">%1$s %2$s/%3$s এর সাথে লিঙ্ক করা হয়েছে</string>\n    <string name=\"export_failed\">রপ্তানি ব্যর্থ: %1$s</string>\n    <string name=\"import_failed\">আমদানি ব্যর্থ: %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d অ্যাপ আমদানি করা হয়েছে</string>\n    <string name=\"imported_skipped\">, %1$d বাদ দেওয়া হয়েছে</string>\n    <string name=\"imported_failed\">, %1$d ব্যর্থ</string>\n    <string name=\"signing_key_changed_title\">সাইনিং কী পরিবর্তিত হয়েছে</string>\n    <string name=\"signing_key_changed_message\">এই অ্যাপের সাইনিং সার্টিফিকেট প্রথম ইনস্টলের পর থেকে পরিবর্তিত হয়েছে।\\n\\nএর অর্থ হতে পারে ডেভেলপার তাদের সাইনিং কী পরিবর্তন করেছে, অথবা বাইনারি পরিবর্তন করা হয়েছে।\\n\\nপ্রত্যাশিত: %1$s\\nপ্রাপ্ত: %2$s</string>\n    <string name=\"install_anyway\">যাই হোক ইনস্টল করুন</string>\n    <string name=\"verified_build\">যাচাইকৃত বিল্ড</string>\n    <string name=\"checking_attestation\">পরীক্ষা হচ্ছে\\u2026</string>\n    <string name=\"assets_title\">সম্পদ</string>\n    <string name=\"no_assets_selected\">কোনো সম্পদ নেই</string>\n    <string name=\"no_assets_in_list\">এই রিলিজের সাথে কোনো সম্পদ যুক্ত নেই</string>\n    <string name=\"assets_selection_label\">সম্পদ বিকল্প নির্বাচন করুন</string>\n    <string name=\"multiple_assets_info_dialog_title\">একাধিক সম্পদ উপলব্ধ</string>\n    <string name=\"multiple_assets_info_dialog_text\">এই রিলিজের জন্য একাধিক ইনস্টলযোগ্য ফাইল উপলব্ধ। তালিকা পর্যালোচনা করুন এবং আপনার ডিভাইসের জন্য উপযুক্তটি নির্বাচন করুন।</string>\n    <string name=\"icon_content_description_info\">তথ্য</string>\n    <string name=\"translation_error_retry\">পুনরায় চেষ্টা</string>\n    <string name=\"translated_from\">স্বয়ংক্রিয়ভাবে শনাক্ত: %1$s</string>\n    <string name=\"select_language\">ভাষা নির্বাচন করুন</string>\n    <string name=\"update_package_mismatch\">প্যাকেজ অমিল: APK হলো %1$s, কিন্তু ইনস্টল করা অ্যাপ হলো %2$s। আপডেট ব্লক করা হয়েছে।</string>\n    <string name=\"update_signing_key_mismatch\">সাইনিং কী অমিল: আপডেটটি একজন ভিন্ন ডেভেলপার দ্বারা সাইন করা হয়েছে। আপডেট ব্লক করা হয়েছে।</string>\n    <string name=\"liquid_glass_option_title\">লিকুইড গ্লাস ইফেক্ট</string>\n    <string name=\"liquid_glass_option_description\">একটি মসৃণ কাচের মতো চেহারা দিয়ে ইন্টারফেস উন্নত করুন</string>\n\n    <string name=\"hide_seen_title\">দেখা রিপোজিটরি লুকান</string>\n    <string name=\"hide_seen_description\">আপনি ইতিমধ্যে দেখেছেন এমন রিপোজিটরি আবিষ্কার ফিড থেকে লুকান</string>\n    <string name=\"clear_seen_history\">দেখার ইতিহাস মুছুন</string>\n    <string name=\"clear_seen_history_description\">সমস্ত দেখা রিপোজিটরি রিসেট করুন যাতে সেগুলো ফিডে আবার দেখা যায়</string>\n    <string name=\"seen_history_cleared\">দেখার ইতিহাস মুছে ফেলা হয়েছে</string>\n    <string name=\"seen_badge\">দেখা হয়েছে</string>\n</resources>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-es/strings-es.xml",
    "content": "<resources>\n    <string name=\"app_name\">GitHub Store</string>\n\n    <string name=\"installed_apps\">Aplicaciones instaladas</string>\n    <string name=\"navigate_back\">Volver</string>\n    <string name=\"check_for_updates\">Buscar actualizaciones</string>\n\n    <string name=\"cannot_launch\">No se puede iniciar %1$s</string>\n    <string name=\"failed_to_open\">No se pudo abrir %1$s</string>\n    <string name=\"failed_to_update\">Error al actualizar %1$s: %2$s</string>\n    <string name=\"update_failed\">Error en la actualización</string>\n    <string name=\"update_all_failed\">Error al actualizar todo: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">Todas las aplicaciones se actualizaron correctamente</string>\n    <string name=\"no_updates_available\">No hay actualizaciones disponibles</string>\n\n    <string name=\"search_your_apps\">Buscar aplicaciones</string>\n    <string name=\"no_apps_found\">No se encontraron aplicaciones</string>\n\n    <string name=\"update_all\">Actualizar todo</string>\n    <string name=\"update\">Actualizar</string>\n    <string name=\"open\">Abrir</string>\n    <string name=\"cancel\">Cancelar</string>\n\n    <string name=\"checking\">Comprobando…</string>\n    <string name=\"updated_successfully\">Actualizado correctamente</string>\n    <string name=\"error_with_message\">Error: %1$s</string>\n\n    <string name=\"updating_x_of_y\">Actualizando %1$d de %2$d</string>\n    <string name=\"currently_updating\">Actual: %1$s</string>\n\n    <string name=\"waiting_for_authorization\">Esperando autorización…</string>\n    <string name=\"signed_in\">¡Sesión iniciada!</string>\n    <string name=\"redirecting_message\">Ya puedes usar la aplicación. Redirigiendo…</string>\n    <string name=\"try_again\">Intentar de nuevo</string>\n\n    <string name=\"auth_error_with_message\">Error: %1$s</string>\n\n    <string name=\"enter_code_on_github\">Introduce este código en GitHub:</string>\n    <string name=\"copy_code\">Copiar código</string>\n    <string name=\"open_github\">Abrir GitHub</string>\n\n    <string name=\"unlock_full_experience\">Desbloquea la experiencia\\ncompleta</string>\n\n    <string name=\"more_requests\">Más solicitudes</string>\n    <string name=\"more_requests_description\">\n        Inicia sesión para obtener límites de API más altos y evitar interrupciones.\n    </string>\n\n    <string name=\"sign_in_with_github\">Iniciar sesión con GitHub</string>\n\n    <string name=\"error_cancelled\">Cancelado</string>\n    <string name=\"error_unknown\">Error desconocido</string>\n\n    <string name=\"language_label\">Idioma:</string>\n\n    <string name=\"discover_repositories\">Descubrir repositorios</string>\n    <string name=\"search_repositories_hint\">Buscar repositorio, descripción…</string>\n\n    <string name=\"filter_by_language\">Filtrar por lenguaje</string>\n\n    <string name=\"results_found\">%1$d resultados encontrados</string>\n\n    <string name=\"retry\">Reintentar</string>\n\n    <string name=\"sort_by\">Ordenar por</string>\n    <string name=\"close\">Cerrar</string>\n\n    <string name=\"sort_most_stars\">Más estrellas</string>\n    <string name=\"sort_most_forks\">Más forks</string>\n    <string name=\"sort_best_match\">Mejor coincidencia</string>\n\n    <string name=\"sort_order_descending\">Descendente</string>\n    <string name=\"sort_order_ascending\">Ascendente</string>\n    <string name=\"sort_label\">Ordenar</string>\n\n    <string name=\"language_all\">Todos los lenguajes</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">La búsqueda falló</string>\n    <string name=\"no_repositories_found\">No se encontraron repositorios</string>\n\n    <string name=\"profile_title\">Perfil</string>\n\n    <string name=\"section_appearance\">APARIENCIA</string>\n    <string name=\"section_about\">ACERCA DE</string>\n    <string name=\"section_network\">RED</string>\n\n    <string name=\"theme_color\">Color del tema</string>\n    <string name=\"amoled_black_theme\">Tema negro AMOLED</string>\n    <string name=\"amoled_black_description\">Fondo negro puro para modo oscuro</string>\n    <string name=\"selected_color\">Color seleccionado: %1$s</string>\n\n    <string name=\"version\">Versión</string>\n    <string name=\"help_support\">Ayuda y soporte</string>\n\n    <string name=\"logout\">Cerrar sesión</string>\n\n    <string name=\"logout_success\">Sesión cerrada correctamente, redirigiendo…</string>\n    <string name=\"cache_cleared\">Caché borrada con éxito</string>\n\n    <string name=\"warning\">¡Advertencia!</string>\n    <string name=\"logout_confirmation\">¿Estás seguro de que deseas cerrar sesión?</string>\n\n    <string name=\"theme_dynamic\">Dinámico</string>\n    <string name=\"theme_ocean\">Océano</string>\n    <string name=\"theme_purple\">Púrpura</string>\n    <string name=\"theme_forest\">Bosque</string>\n    <string name=\"theme_slate\">Pizarra</string>\n    <string name=\"theme_amber\">Ámbar</string>\n\n    <string name=\"error_loading_details\">Error al cargar detalles</string>\n    <string name=\"about_this_app\">Acerca de esta app</string>\n    <string name=\"install_logs\">Registros de instalación</string>\n    <string name=\"author\">Autor</string>\n    <string name=\"whats_new\">Novedades</string>\n\n    <string name=\"installed\">Instalado</string>\n    <string name=\"update_available\">Actualización disponible</string>\n    <string name=\"install_latest\">Instalar última versión</string>\n    <string name=\"reinstall\">Reinstalar</string>\n\n    <string name=\"downloading\">Descargando</string>\n    <string name=\"updating\">Actualizando</string>\n    <string name=\"verifying\">Verificando</string>\n    <string name=\"installing\">Instalando</string>\n\n    <string name=\"profile\">Perfil</string>\n    <string name=\"forks\">Bifurcaciones</string>\n    <string name=\"stars\">Estrellas</string>\n    <string name=\"issues\">Problemas</string>\n\n    <string name=\"by_author\">por %1$s</string>\n    <string name=\"installed_version\">• Instalado: %1$s</string>\n    <string name=\"architecture_compatible\">Arquitectura compatible</string>\n    <string name=\"update_to_version\">Actualizar a %1$s</string>\n\n    <string name=\"error_load_details\">No se pudieron cargar los detalles</string>\n    <string name=\"installer_saved_downloads\">El instalador se guardó en Descargas</string>\n\n    <string name=\"log_download_started\">Descarga iniciada</string>\n    <string name=\"log_downloaded\">Descargado</string>\n    <string name=\"log_update_started\">Actualización iniciada</string>\n    <string name=\"log_installed\">Instalado</string>\n    <string name=\"log_updated\">Actualizado</string>\n    <string name=\"log_cancelled\">Cancelado</string>\n    <string name=\"log_install_started\">Instalación iniciada</string>\n    <string name=\"log_error\">Error</string>\n    <string name=\"log_error_with_message\">Error: %1$s</string>\n\n\n    <string name=\"log_prepare_appmanager\">Preparando para AppManager</string>\n    <string name=\"log_opened_appmanager\">Abierto en AppManager</string>\n    <string name=\"log_permission_blocked\">Permiso de instalación bloqueado por política del dispositivo</string>\n    <string name=\"log_opened_external_installer\">Abierto en instalador externo</string>\n    <string name=\"install_permission_unavailable\">Permiso de instalación no disponible</string>\n    <string name=\"install_permission_blocked_message\">El APK se descargó correctamente pero este dispositivo no permite la instalación directa. ¿Desea abrirlo con un instalador externo?</string>\n    <string name=\"open_with_external_installer\">Abrir con instalador externo</string>\n    <string name=\"external_installer_description\">Usar una aplicación de terceros para instalar el APK</string>\n\n    <string name=\"error_generic\">Error: %1$s</string>\n    <string name=\"error_asset_not_supported\">Tipo de archivo .%1$s no compatible</string>\n    <string name=\"error_file_not_found\">Archivo descargado no encontrado</string>\n\n    <string name=\"home_category_trending\">Tendencias</string>\n    <string name=\"home_category_hot_release\">Lanzamiento en caliente</string>\n    <string name=\"home_category_most_popular\">Los más populares</string>\n\n    <string name=\"home_finding_repositories\">Buscando repositorios...</string>\n    <string name=\"home_loading_more\">Cargando...</string>\n    <string name=\"home_no_more_repositories\">No hay más repositorios</string>\n    <string name=\"home_retry\">Reintentar</string>\n    <string name=\"home_failed_to_load_repositories\">No se pudieron cargar los repositorios</string>\n    <string name=\"home_view_details\">Ver detalles</string>\n\n    <string name=\"updated_just_now\">actualizado ahora mismo</string>\n    <string name=\"updated_hours_ago\">actualizado hace %1$d h</string>\n    <string name=\"updated_yesterday\">actualizado ayer</string>\n    <string name=\"updated_days_ago\">actualizado hace %1$d días</string>\n    <string name=\"updated_on_date\">actualizado el %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">Límite de solicitudes excedido</string>\n    <string name=\"rate_limit_used_all\">Has usado las %1$d solicitudes de la API.</string>\n    <string name=\"rate_limit_used_all_free\">Has usado las %1$d solicitudes gratuitas de la API.</string>\n    <string name=\"rate_limit_resets_in_minutes\">Se restablece en %1$d minutos</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Inicia sesión para obtener 5 000 solicitudes por hora en lugar de 60.</string>\n    <string name=\"rate_limit_sign_in\">Iniciar sesión</string>\n    <string name=\"rate_limit_ok\">OK</string>\n    <string name=\"rate_limit_close\">Cerrar</string>\n\n    <string name=\"system_font\">Fuente del sistema</string>\n    <string name=\"system_font_description\">Usa la fuente de tu dispositivo para mejor legibilidad</string>\n\n    <string name=\"theme_light\">Claro</string>\n    <string name=\"theme_dark\">Oscuro</string>\n    <string name=\"theme_system\">Sistema</string>\n\n    <string name=\"added_to_favourites\">Repositorio añadido a favoritos</string>\n    <string name=\"removed_from_favourites\">Repositorio eliminado de favoritos</string>\n    <string name=\"add_to_favourites\">Añadir a favoritos</string>\n    <string name=\"remove_from_favourites\">Quitar de favoritos</string>\n    <string name=\"favourites\">Favoritos</string>\n\n    <string name=\"added_just_now\">añadido justo ahora</string>\n    <string name=\"added_hours_ago\">añadido hace %1$d hora(s)</string>\n    <string name=\"added_yesterday\">añadido ayer</string>\n    <string name=\"added_days_ago\">añadido hace %1$d día(s)</string>\n    <string name=\"added_on_date\">añadido el %1$s</string>\n\n    <string name=\"starred_repositories\">Repositorios destacados</string>\n    <string name=\"repository_starred\">El repositorio está marcado con estrella</string>\n    <string name=\"repository_not_starred\">El repositorio no está marcado con estrella</string>\n    <string name=\"star_from_github\">Puedes marcar el repositorio desde GitHub</string>\n    <string name=\"unstar_from_github\">Puedes quitar la estrella desde GitHub</string>\n\n    <string name=\"sign_in_required\">Inicio de sesión requerido</string>\n    <string name=\"sign_in_with_github_for_stars\">Inicia sesión con GitHub para ver tus repositorios destacados</string>\n    <string name=\"no_starred_repos\">No hay repositorios destacados</string>\n    <string name=\"star_repos_hint\">Marca repositorios con lanzamientos instalables en GitHub para verlos aquí</string>\n    <string name=\"last_synced\">Última sincronización</string>\n    <string name=\"just_now\">Justo ahora</string>\n    <string name=\"minutes_ago\">Hace %d min</string>\n    <string name=\"hours_ago\">Hace %d h</string>\n    <string name=\"days_ago\">Hace %d d</string>\n    <string name=\"dismiss\">Cerrar</string>\n    <string name=\"sync_starred_failed\">No se pudieron sincronizar los repositorios destacados</string>\n\n    <string name=\"developer_profile_title\">Perfil del desarrollador</string>\n    <string name=\"open_developer_profile\">Abrir perfil de desarrollador</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">Error al cargar repositorios</string>\n    <string name=\"failed_to_load_profile\">Error al cargar perfil</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Repositorios</string>\n    <string name=\"followers\">Seguidores</string>\n    <string name=\"following\">Siguiendo</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Buscar repositorios…</string>\n    <string name=\"clear_search\">Borrar búsqueda</string>\n    <string name=\"filter_all\">Todos</string>\n    <string name=\"filter_with_releases\">Con lanzamientos</string>\n    <string name=\"filter_installed\">Instalados</string>\n    <string name=\"filter_favorites\">Favoritos</string>\n    <string name=\"sort\">Ordenar</string>\n    <string name=\"sort_recently_updated\">Actualizados recientemente</string>\n    <string name=\"sort_name\">Nombre</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">repositorio</string>\n    <string name=\"repositories_plural\">repositorios</string>\n    <string name=\"showing_x_of_y_repositories\">Mostrando %1$d de %2$d repositorios</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">No hay repositorios con lanzamientos instalables</string>\n    <string name=\"no_installed_repos\">No hay repositorios instalados</string>\n    <string name=\"no_favorite_repos\">No hay repositorios favoritos</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">Actualizado hace %1$s</string>\n    <string name=\"has_release\">Tiene lanzamiento</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">hace %1$d año(s)</string>\n    <string name=\"time_months_ago\">hace %1$d mes(es)</string>\n    <string name=\"time_days_ago\">hace %1$d d</string>\n    <string name=\"time_hours_ago\">hace %1$d h</string>\n    <string name=\"time_minutes_ago\">hace %1$d min</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">Publicado ahora mismo</string>\n    <string name=\"released_hours_ago\">Publicado hace %1$d hora(s)</string>\n    <string name=\"released_yesterday\">Publicado ayer</string>\n    <string name=\"released_days_ago\">Publicado hace %1$d día(s)</string>\n    <string name=\"released_on_date\">Publicado el %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">Inicio</string>\n    <string name=\"bottom_nav_search_title\">Buscar</string>\n    <string name=\"bottom_nav_apps_title\">Aplicaciones</string>\n    <string name=\"bottom_nav_profile_title\">Perfil</string>\n\n    <string name=\"forked_repository\">Bifurcar</string>\n\n    <string name=\"category_stable\">Estable</string>\n    <string name=\"category_pre_release\">Prelanzamiento</string>\n    <string name=\"category_all\">Todos</string>\n    <string name=\"select_version\">Seleccionar versión</string>\n    <string name=\"pre_release_badge\">Prelanzamiento</string>\n    <string name=\"no_version_selected\">Ninguna versión seleccionada</string>\n    <string name=\"versions_title\">Versiones</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">Abrir repositorio</string>\n    <string name=\"open_in_browser\">Abrir en navegador</string>\n    <string name=\"cancel_download\">Cancelar descarga</string>\n    <string name=\"show_install_options\">Mostrar opciones de instalación</string>\n\n    <!-- Errors & states -->\n    <string name=\"no_description\">Sin descripción proporcionada.</string>\n    <string name=\"no_release_notes\">Sin notas de versión.</string>\n    <string name=\"report_issue\">Reportar problema</string>\n\n    <!-- Install status -->\n    <string name=\"not_available\">No disponible</string>\n    <string name=\"update_app\">Actualizar app</string>\n    <string name=\"pending_install\">Instalación pendiente</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Abrir en Obtainium</string>\n    <string name=\"obtainium_description\">Gestionar actualizaciones automáticamente</string>\n    <string name=\"inspect_with_appmanager\">Inspeccionar con AppManager</string>\n    <string name=\"appmanager_description\">Verificar permisos, rastreadores y seguridad</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Desinstalar</string>\n    <string name=\"open_app\">Abrir</string>\n    <string name=\"downgrade_requires_uninstall\">La degradación requiere desinstalar</string>\n    <string name=\"downgrade_warning_message\">Instalar la versión %1$s requiere desinstalar la versión actual (%2$s) primero. Los datos de la app se perderán.</string>\n    <string name=\"uninstall_first\">Desinstalar primero</string>\n    <string name=\"install_version\">Instalar %1$s</string>\n    <string name=\"failed_to_open_app\">Error al abrir %1$s</string>\n    <string name=\"failed_to_uninstall\">Error al desinstalar %1$s</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">Última</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Última comprobación: %1$s</string>\n    <string name=\"last_checked_never\">Nunca comprobado</string>\n    <string name=\"last_checked_just_now\">justo ahora</string>\n    <string name=\"last_checked_minutes_ago\">hace %1$d min</string>\n    <string name=\"last_checked_hours_ago\">hace %1$d h</string>\n    <string name=\"checking_for_updates\">Comprobando actualizaciones…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Tipo de proxy</string>\n    <string name=\"proxy_none\">Ninguno</string>\n    <string name=\"proxy_system\">Sistema</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Host</string>\n    <string name=\"proxy_port\">Puerto</string>\n    <string name=\"proxy_username\">Nombre de usuario (opcional)</string>\n    <string name=\"proxy_password\">Contraseña (opcional)</string>\n    <string name=\"proxy_save\">Guardar proxy</string>\n    <string name=\"proxy_saved\">Configuración de proxy guardada</string>\n    <string name=\"proxy_system_description\">Usa la configuración de proxy del dispositivo</string>\n    <string name=\"proxy_port_error\">El puerto debe ser 1–65535</string>\n    <string name=\"proxy_none_description\">Conexión directa, sin proxy</string>\n    <string name=\"failed_to_save_proxy_settings\">No se pudieron guardar los ajustes del proxy</string>\n    <string name=\"proxy_host_required\">Se requiere el host del proxy</string>\n    <string name=\"invalid_proxy_port\">Puerto de proxy no válido</string>\n    <string name=\"proxy_show_password\">Mostrar contraseña</string>\n    <string name=\"proxy_hide_password\">Ocultar contraseña</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Rastrear esta app</string>\n    <string name=\"app_tracked_successfully\">App añadida a la lista de seguimiento</string>\n    <string name=\"failed_to_track_app\">Error al rastrear la app: %1$s</string>\n    <string name=\"already_tracked\">La app ya está siendo rastreada</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">Iniciar sesión en GitHub</string>\n    <string name=\"profile_sign_in_description\">Desbloquea la experiencia completa. Gestiona tus apps, sincroniza tus preferencias y navega más rápido.</string>\n    <string name=\"profile_repos\">Repos</string>\n    <string name=\"profile_login\">Iniciar sesión</string>\n    <string name=\"profile_stars_description\">Tus repositorios destacados de GitHub</string>\n    <string name=\"profile_favourites_description\">Tus repositorios favoritos guardados localmente</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Sesión expirada</string>\n    <string name=\"session_expired_message\">Tu sesión de GitHub ha expirado o el token fue revocado. Inicia sesión nuevamente para continuar usando las funciones autenticadas.</string>\n    <string name=\"session_expired_hint\">Puedes seguir navegando como invitado con solicitudes de API limitadas.</string>\n    <string name=\"sign_in_again\">Iniciar sesión de nuevo</string>\n    <string name=\"continue_as_guest\">Continuar como invitado</string>\n    <string name=\"logout_revocation_note\">Esto borrará tu sesión local y los datos en caché. Para revocar el acceso completamente, visita GitHub Settings > Applications.</string>\n    <string name=\"auth_code_expires_in\">El código expira en %1$s</string>\n    <string name=\"auth_error_code_expired\">El código del dispositivo ha expirado.</string>\n    <string name=\"auth_hint_try_again\">Intenta iniciar sesión de nuevo para obtener un nuevo código.</string>\n    <string name=\"auth_hint_check_connection\">Revisa tu conexión a internet e intenta de nuevo.</string>\n    <string name=\"auth_hint_denied\">Rechazaste la solicitud de autorización. Intenta de nuevo si fue involuntario.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Leer más</string>\n    <string name=\"show_less\">Mostrar menos</string>\n\n    <string name=\"share_repository\">Compartir repositorio</string>\n    <string name=\"failed_to_share_link\">No se pudo compartir el enlace</string>\n    <string name=\"link_copied_to_clipboard\">Enlace copiado al portapapeles</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Traducir</string>\n    <string name=\"translating\">Traduciendo…</string>\n    <string name=\"show_original\">Mostrar original</string>\n    <string name=\"translated_to\">Traducido a %1$s</string>\n    <string name=\"translate_to\">Traducir a…</string>\n    <string name=\"search_language\">Buscar idioma</string>\n    <string name=\"change_language\">Cambiar idioma</string>\n    <string name=\"translation_failed\">Error de traducción. Inténtalo de nuevo.</string>\n\n    <string name=\"open_github_link\">Abrir enlace de GitHub</string>\n    <string name=\"clipboard_link_detected\">Enlace de GitHub detectado en el portapapeles</string>\n    <string name=\"auto_detect_clipboard_links\">Detectar enlaces del portapapeles</string>\n    <string name=\"auto_detect_clipboard_description\">Detectar automáticamente enlaces de GitHub del portapapeles al abrir la búsqueda</string>\n    <string name=\"detected_links\">Enlaces detectados</string>\n    <string name=\"open_in_app\">Abrir en la app</string>\n    <string name=\"no_github_link_in_clipboard\">No se encontró enlace de GitHub en el portapapeles</string>\n\n    <string name=\"storage\">Almacenamiento</string>\n    <string name=\"clear_cache\">Borrar caché</string>\n    <string name=\"current_size\">Tamaño actual:</string>\n    <string name=\"clear\">Borrar</string>\n\n    <string name=\"sponsor_title\">Apoya GitHub Store</string>\n    <string name=\"sponsor_button\">Apoyar el proyecto</string>\n    <string name=\"sponsor_hero_title\">Creado con amor,\\nmantenido con café</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store ha alcanzado más de 130,000 descargas y 7,700 estrellas en GitHub — 100% gratis, sin anuncios ni rastreo.</string>\n    <string name=\"sponsor_personal_note\">Construí y mantengo esto completamente por mi cuenta mientras termino la secundaria. Tu apoyo — incluso una pequeña cantidad — ayuda a mantener la app sin errores, pagar la infraestructura y lanzar las funciones que solicitan.</string>\n\n    <string name=\"sponsor_kodee_title\">¡Vota por GitHub Store!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store está nominado a los Golden Kodee Awards en KotlinConf 2026. Tu voto toma solo 2 minutos y significa mucho.</string>\n    <string name=\"sponsor_kodee_register\">1. Registrarse</string>\n    <string name=\"sponsor_kodee_vote\">2. Votar</string>\n    <string name=\"sponsor_kodee_deadline\">La votación cierra el 22 de marzo</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Regístrate en la plataforma de premios (Continuar con Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. Toca Votar abajo para abrir la página</string>\n    <string name=\"sponsor_kodee_step3\">3. Busca a Usmon Narzullayev y pulsa Votar</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">Apoyo recurrente o único mediante GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Apoyo rápido de una sola vez</string>\n\n    <string name=\"sponsor_other_ways_title\">OTRAS FORMAS DE AYUDAR</string>\n\n    <string name=\"sponsor_star_repo\">Dar estrella al repositorio</string>\n    <string name=\"sponsor_star_repo_desc\">Ayuda a otros a descubrir GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">Reportar errores</string>\n    <string name=\"sponsor_report_bugs_desc\">Mejora la app para todos</string>\n\n    <string name=\"sponsor_share\">Compartir con amigos</string>\n    <string name=\"sponsor_share_desc\">Corre la voz entre otros desarrolladores</string>\n\n    <string name=\"sponsor_thank_you\">Cada forma de apoyo — financiera o no — mantiene vivo este proyecto. ¡Gracias!</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Instalación</string>\n    <string name=\"installer_type_default\">Predeterminado</string>\n    <string name=\"installer_type_default_description\">Diálogo de instalación estándar del sistema</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Instalación silenciosa sin confirmaciones</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku no está instalado</string>\n    <string name=\"shizuku_status_not_running\">Shizuku no está en ejecución</string>\n    <string name=\"shizuku_status_permission_needed\">Permiso requerido</string>\n    <string name=\"shizuku_status_ready\">Listo</string>\n    <string name=\"shizuku_grant_permission\">Conceder permiso</string>\n    <string name=\"shizuku_install_hint\">Instala Shizuku para habilitar la instalación silenciosa</string>\n    <string name=\"shizuku_start_hint\">Inicia Shizuku para habilitar la instalación silenciosa</string>\n    <string name=\"shizuku_install_failed_fallback\">La instalación con Shizuku falló, usando el instalador estándar</string>\n\n    <string name=\"auto_update_title\">Actualizar apps automáticamente</string>\n    <string name=\"auto_update_description\">Descargar e instalar actualizaciones en segundo plano a través de Shizuku</string>\n\n    <string name=\"section_updates\">Actualizaciones</string>\n    <string name=\"update_check_interval_title\">Intervalo de verificación</string>\n    <string name=\"update_check_interval_description\">Con qué frecuencia buscar actualizaciones en segundo plano</string>\n    <string name=\"interval_3h\">3h</string>\n    <string name=\"interval_6h\">6h</string>\n    <string name=\"interval_12h\">12h</string>\n    <string name=\"interval_24h\">24h</string>\n\n    <string name=\"add_by_link\">Añadir por enlace</string>\n    <string name=\"link_app_title\">Vincular app al repositorio</string>\n    <string name=\"pick_installed_app\">Elige una app instalada para vincularla a un repositorio de GitHub</string>\n    <string name=\"search_apps_hint\">Buscar apps…</string>\n    <string name=\"enter_repo_url\">URL del repositorio GitHub</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Validando…</string>\n    <string name=\"link_and_track\">Vincular y seguir</string>\n    <string name=\"checking_release\">Comprobando último lanzamiento…</string>\n    <string name=\"downloading_for_verification\">Descargando APK para verificación…</string>\n    <string name=\"verifying_signing_key\">Verificando clave de firma…</string>\n    <string name=\"package_name_mismatch\">Nombre de paquete no coincide: el APK es %1$s, pero la app seleccionada es %2$s</string>\n    <string name=\"signing_key_mismatch_link\">Clave de firma no coincide: el APK de este repositorio fue firmado por un desarrollador diferente</string>\n    <string name=\"select_asset_title\">Seleccionar instalador</string>\n    <string name=\"select_asset_description\">Elige el APK para verificar contra tu app instalada</string>\n    <string name=\"download_failed\">Error en la descarga</string>\n    <string name=\"export_apps\">Exportar</string>\n    <string name=\"import_apps\">Importar</string>\n    <string name=\"import_apps_title\">Importar apps</string>\n    <string name=\"import_apps_description\">Pega el JSON exportado para restaurar tus apps rastreadas</string>\n    <string name=\"import_apps_hint\">Pega el JSON exportado aquí…</string>\n    <string name=\"include_pre_releases_title\">Incluir pre-lanzamientos</string>\n    <string name=\"include_pre_releases_description\">Rastrear versiones pre-lanzamiento al buscar actualizaciones. Si está desactivado, solo se consideran las versiones estables.</string>\n    <string name=\"confirm_uninstall_title\">¿Desinstalar app?</string>\n    <string name=\"confirm_uninstall_message\">¿Estás seguro de que quieres desinstalar %1$s? Esta acción no se puede deshacer y los datos de la app podrían perderse.</string>\n    <string name=\"invalid_github_url\">URL de GitHub no válida. Usa el formato: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Repositorio no encontrado: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">Límite de la API de GitHub excedido. Inténtalo más tarde.</string>\n    <string name=\"failed_to_link\">Error al vincular: %1$s</string>\n    <string name=\"failed_to_load_apps\">Error al cargar las apps instaladas</string>\n    <string name=\"app_linked_success\">%1$s vinculada a %2$s/%3$s</string>\n    <string name=\"export_failed\">Error en la exportación: %1$s</string>\n    <string name=\"import_failed\">Error en la importación: %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d apps importadas</string>\n    <string name=\"imported_skipped\">, %1$d omitidas</string>\n    <string name=\"imported_failed\">, %1$d fallidas</string>\n    <string name=\"signing_key_changed_title\">Clave de firma cambiada</string>\n    <string name=\"signing_key_changed_message\">El certificado de firma de esta app ha cambiado desde su primera instalación.\\n\\nEsto podría significar que el desarrollador rotó su clave de firma, o el binario pudo haber sido manipulado.\\n\\nEsperado: %1$s\\nRecibido: %2$s</string>\n    <string name=\"install_anyway\">Instalar de todos modos</string>\n    <string name=\"verified_build\">Compilación verificada</string>\n    <string name=\"checking_attestation\">Comprobando\\u2026</string>\n    <string name=\"assets_title\">Recursos</string>\n    <string name=\"no_assets_selected\">Sin recursos</string>\n    <string name=\"no_assets_in_list\">No hay recursos asociados a este lanzamiento</string>\n    <string name=\"assets_selection_label\">Seleccionar opción de recurso</string>\n    <string name=\"multiple_assets_info_dialog_title\">Múltiples recursos disponibles</string>\n    <string name=\"multiple_assets_info_dialog_text\">Hay varios archivos instalables disponibles para este lanzamiento. Revisa la lista y selecciona el que se ajuste a tu dispositivo.</string>\n    <string name=\"icon_content_description_info\">Información</string>\n    <string name=\"translation_error_retry\">Reintentar</string>\n    <string name=\"translated_from\">Detectado automáticamente: %1$s</string>\n    <string name=\"select_language\">Seleccionar idioma</string>\n    <string name=\"update_package_mismatch\">Paquete no coincide: el APK es %1$s, pero la aplicación instalada es %2$s. Actualización bloqueada.</string>\n    <string name=\"update_signing_key_mismatch\">Clave de firma no coincide: la actualización fue firmada por un desarrollador diferente. Actualización bloqueada.</string>\n    <string name=\"liquid_glass_option_title\">Efecto de cristal líquido</string>\n    <string name=\"liquid_glass_option_description\">Mejora la interfaz con una apariencia suave tipo cristal</string>\n\n    <string name=\"hide_seen_title\">Ocultar repositorios vistos</string>\n    <string name=\"hide_seen_description\">Ocultar repositorios que ya has visto de las fuentes de descubrimiento</string>\n    <string name=\"clear_seen_history\">Borrar historial de vistos</string>\n    <string name=\"clear_seen_history_description\">Restablecer todos los repositorios vistos para que aparezcan de nuevo en las fuentes</string>\n    <string name=\"seen_history_cleared\">Historial de vistos borrado</string>\n    <string name=\"seen_badge\">Visto</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml",
    "content": "<resources>\n    <string name=\"app_name\">GitHub Store</string>\n\n    <string name=\"installed_apps\">Applications installées</string>\n    <string name=\"navigate_back\">Retour</string>\n    <string name=\"check_for_updates\">Vérifier les mises à jour</string>\n\n    <string name=\"cannot_launch\">Impossible de lancer %1$s</string>\n    <string name=\"failed_to_open\">Impossible d’ouvrir %1$s</string>\n    <string name=\"failed_to_update\">Échec de la mise à jour de %1$s : %2$s</string>\n    <string name=\"update_failed\">Échec de la mise à jour</string>\n    <string name=\"update_all_failed\">Échec de la mise à jour globale : %1$s</string>\n    <string name=\"all_apps_updated_successfully\">Toutes les applications ont été mises à jour</string>\n    <string name=\"no_updates_available\">Aucune mise à jour disponible</string>\n\n    <string name=\"search_your_apps\">Rechercher des applications</string>\n    <string name=\"no_apps_found\">Aucune application trouvée</string>\n\n    <string name=\"update_all\">Tout mettre à jour</string>\n    <string name=\"update\">Mettre à jour</string>\n    <string name=\"open\">Ouvrir</string>\n    <string name=\"cancel\">Annuler</string>\n\n    <string name=\"checking\">Vérification…</string>\n    <string name=\"updated_successfully\">Mise à jour réussie</string>\n    <string name=\"error_with_message\">Erreur : %1$s</string>\n\n    <string name=\"updating_x_of_y\">Mise à jour %1$d sur %2$d</string>\n    <string name=\"currently_updating\">Actuellement : %1$s</string>\n\n    <string name=\"waiting_for_authorization\">En attente d’autorisation…</string>\n    <string name=\"signed_in\">Connecté !</string>\n    <string name=\"redirecting_message\">Vous pouvez maintenant utiliser l’application. Redirection…</string>\n    <string name=\"try_again\">Réessayer</string>\n\n    <string name=\"auth_error_with_message\">Erreur : %1$s</string>\n\n    <string name=\"enter_code_on_github\">Entrez ce code sur GitHub :</string>\n    <string name=\"copy_code\">Copier le code</string>\n    <string name=\"open_github\">Ouvrir GitHub</string>\n\n    <string name=\"unlock_full_experience\">Débloquez l’expérience\\ncomplète</string>\n\n    <string name=\"more_requests\">Plus de requêtes</string>\n    <string name=\"more_requests_description\">\n        Connectez-vous pour obtenir des limites API plus élevées et éviter les interruptions.\n    </string>\n\n    <string name=\"sign_in_with_github\">Se connecter avec GitHub</string>\n\n    <string name=\"error_cancelled\">Annulé</string>\n    <string name=\"error_unknown\">Erreur inconnue</string>\n\n    <string name=\"language_label\">Langue :</string>\n\n    <string name=\"discover_repositories\">Découvrir des dépôts</string>\n    <string name=\"search_repositories_hint\">Rechercher dépôt, description…</string>\n\n    <string name=\"filter_by_language\">Filtrer par langage</string>\n\n    <string name=\"results_found\">%1$d résultats trouvés</string>\n\n    <string name=\"retry\">Réessayer</string>\n\n    <string name=\"sort_by\">Trier par</string>\n    <string name=\"close\">Fermer</string>\n\n    <string name=\"sort_most_stars\">Le plus d’étoiles</string>\n    <string name=\"sort_most_forks\">Le plus de forks</string>\n    <string name=\"sort_best_match\">Meilleure correspondance</string>\n\n    <string name=\"sort_order_descending\">Décroissant</string>\n    <string name=\"sort_order_ascending\">Croissant</string>\n    <string name=\"sort_label\">Trier</string>\n\n    <string name=\"language_all\">Tous les langages</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">La recherche a échoué</string>\n    <string name=\"no_repositories_found\">Aucun dépôt trouvé</string>\n\n    <string name=\"profile_title\">Profil</string>\n\n    <string name=\"section_appearance\">APPARENCE</string>\n    <string name=\"section_about\">À PROPOS</string>\n    <string name=\"section_network\">RÉSEAU</string>\n\n    <string name=\"theme_color\">Couleur du thème</string>\n    <string name=\"amoled_black_theme\">Thème noir AMOLED</string>\n    <string name=\"amoled_black_description\">Fond noir pur pour le mode sombre</string>\n    <string name=\"selected_color\">Couleur sélectionnée : %1$s</string>\n\n    <string name=\"version\">Version</string>\n    <string name=\"help_support\">Aide et support</string>\n\n    <string name=\"logout\">Se déconnecter</string>\n\n    <string name=\"logout_success\">Déconnexion réussie, redirection…</string>\n    <string name=\"cache_cleared\">Cache vidé avec succès</string>\n\n    <string name=\"warning\">Attention !</string>\n    <string name=\"logout_confirmation\">Voulez-vous vraiment vous déconnecter ?</string>\n\n    <string name=\"theme_dynamic\">Dynamique</string>\n    <string name=\"theme_ocean\">Océan</string>\n    <string name=\"theme_purple\">Violet</string>\n    <string name=\"theme_forest\">Forêt</string>\n    <string name=\"theme_slate\">Ardoise</string>\n    <string name=\"theme_amber\">Ambre</string>\n\n    <string name=\"error_loading_details\">Erreur de chargement</string>\n    <string name=\"about_this_app\">À propos de cette application</string>\n    <string name=\"install_logs\">Journaux d’installation</string>\n    <string name=\"author\">Auteur</string>\n    <string name=\"whats_new\">Nouveautés</string>\n\n    <string name=\"installed\">Installé</string>\n    <string name=\"update_available\">Mise à jour disponible</string>\n    <string name=\"install_latest\">Installer la dernière version</string>\n    <string name=\"reinstall\">Réinstaller</string>\n\n    <string name=\"downloading\">Téléchargement</string>\n    <string name=\"updating\">Mise à jour</string>\n    <string name=\"verifying\">Vérification</string>\n    <string name=\"installing\">Installation</string>\n\n    <string name=\"profile\">Profil</string>\n    <string name=\"forks\">Forks</string>\n    <string name=\"stars\">Étoiles</string>\n    <string name=\"issues\">Tickets</string>\n\n    <string name=\"by_author\">par %1$s</string>\n    <string name=\"installed_version\">• Installé : %1$s</string>\n    <string name=\"architecture_compatible\">Architecture compatible</string>\n    <string name=\"update_to_version\">Mettre à jour vers %1$s</string>\n\n    <string name=\"error_load_details\">Impossible de charger les détails</string>\n    <string name=\"installer_saved_downloads\">L’installateur a été enregistré dans Téléchargements</string>\n\n    <string name=\"log_download_started\">Téléchargement démarré</string>\n    <string name=\"log_downloaded\">Téléchargé</string>\n    <string name=\"log_update_started\">Mise à jour démarrée</string>\n    <string name=\"log_installed\">Installé</string>\n    <string name=\"log_updated\">Mis à jour</string>\n    <string name=\"log_cancelled\">Annulé</string>\n    <string name=\"log_install_started\">Installation démarrée</string>\n    <string name=\"log_error\">Erreur</string>\n    <string name=\"log_error_with_message\">Erreur : %1$s</string>\n\n\n    <string name=\"log_prepare_appmanager\">Préparation pour AppManager</string>\n    <string name=\"log_opened_appmanager\">Ouvert dans AppManager</string>\n    <string name=\"log_permission_blocked\">Permission d\\'installation bloquée par la politique de l\\'appareil</string>\n    <string name=\"log_opened_external_installer\">Ouvert dans l\\'installateur externe</string>\n    <string name=\"install_permission_unavailable\">Permission d\\'installation indisponible</string>\n    <string name=\"install_permission_blocked_message\">L\\'APK a été téléchargé avec succès mais cet appareil n\\'autorise pas l\\'installation directe. Voulez-vous l\\'ouvrir avec un installateur externe ?</string>\n    <string name=\"open_with_external_installer\">Ouvrir avec un installateur externe</string>\n    <string name=\"external_installer_description\">Utiliser une application tierce pour installer l\\'APK</string>\n\n    <string name=\"error_generic\">Erreur : %1$s</string>\n    <string name=\"error_asset_not_supported\">Type de fichier .%1$s non pris en charge</string>\n    <string name=\"error_file_not_found\">Fichier téléchargé introuvable</string>\n\n    <string name=\"home_category_trending\">Tendances</string>\n    <string name=\"home_category_hot_release\">Sortie en salles</string>\n    <string name=\"home_category_most_popular\">Les plus populaires</string>\n\n    <string name=\"home_finding_repositories\">Recherche de dépôts...</string>\n    <string name=\"home_loading_more\">Chargement...</string>\n    <string name=\"home_no_more_repositories\">Plus aucun dépôt</string>\n    <string name=\"home_retry\">Réessayer</string>\n    <string name=\"home_failed_to_load_repositories\">Impossible de charger les dépôts</string>\n    <string name=\"home_view_details\">Voir les détails</string>\n\n    <string name=\"updated_just_now\">mis à jour à l’instant</string>\n    <string name=\"updated_hours_ago\">mis à jour il y a %1$d h</string>\n    <string name=\"updated_yesterday\">mis à jour hier</string>\n    <string name=\"updated_days_ago\">mis à jour il y a %1$d j</string>\n    <string name=\"updated_on_date\">mis à jour le %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">Limite de requêtes dépassée</string>\n    <string name=\"rate_limit_used_all\">Vous avez utilisé les %1$d requêtes API.</string>\n    <string name=\"rate_limit_used_all_free\">Vous avez utilisé les %1$d requêtes API gratuites.</string>\n    <string name=\"rate_limit_resets_in_minutes\">Réinitialisation dans %1$d minutes</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Connectez-vous pour obtenir 5 000 requêtes par heure au lieu de 60 !</string>\n    <string name=\"rate_limit_sign_in\">Se connecter</string>\n    <string name=\"rate_limit_ok\">OK</string>\n    <string name=\"rate_limit_close\">Fermer</string>\n\n    <string name=\"system_font\">Police système</string>\n    <string name=\"system_font_description\">Utilisez la police de votre appareil pour une meilleure lisibilité</string>\n\n    <string name=\"theme_light\">Clair</string>\n    <string name=\"theme_dark\">Sombre</string>\n    <string name=\"theme_system\">Système</string>\n\n    <string name=\"added_to_favourites\">Dépôt ajouté aux favoris</string>\n    <string name=\"removed_from_favourites\">Dépôt supprimé des favoris</string>\n    <string name=\"add_to_favourites\">Ajouter aux favoris</string>\n    <string name=\"remove_from_favourites\">Retirer des favoris</string>\n    <string name=\"favourites\">Favoris</string>\n\n    <string name=\"added_just_now\">ajouté à l’instant</string>\n    <string name=\"added_hours_ago\">ajouté il y a %1$d heure(s)</string>\n    <string name=\"added_yesterday\">ajouté hier</string>\n    <string name=\"added_days_ago\">ajouté il y a %1$d jour(s)</string>\n    <string name=\"added_on_date\">ajouté le %1$s</string>\n\n    <string name=\"starred_repositories\">Dépôts Étoilés</string>\n    <string name=\"repository_starred\">Le dépôt est en favori</string>\n    <string name=\"repository_not_starred\">Le dépôt n’est pas en favori</string>\n    <string name=\"star_from_github\">Vous pouvez ajouter ce dépôt aux favoris sur GitHub</string>\n    <string name=\"unstar_from_github\">Vous pouvez retirer ce dépôt des favoris sur GitHub</string>\n\n    <string name=\"sign_in_required\">Connexion requise</string>\n    <string name=\"no_starred_repos\">Aucun dépôt favori</string>\n    <string name=\"sign_in_with_github_for_stars\">Connectez-vous avec GitHub pour voir vos dépôts favoris</string>\n    <string name=\"star_repos_hint\">Ajoutez des dépôts avec des versions installables sur GitHub pour les voir ici</string>\n    <string name=\"last_synced\">Dernière synchronisation</string>\n    <string name=\"just_now\">À l’instant</string>\n    <string name=\"minutes_ago\">Il y a %d min</string>\n    <string name=\"hours_ago\">Il y a %d h</string>\n    <string name=\"days_ago\">Il y a %d j</string>\n    <string name=\"dismiss\">Fermer</string>\n    <string name=\"sync_starred_failed\">Échec de la synchronisation des dépôts favoris</string>\n\n    <string name=\"developer_profile_title\">Profil du développeur</string>\n    <string name=\"open_developer_profile\">Ouvrir le profil du développeur</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">Échec du chargement des dépôts</string>\n    <string name=\"failed_to_load_profile\">Échec du chargement du profil</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Dépôts</string>\n    <string name=\"followers\">Abonnés</string>\n    <string name=\"following\">Abonnements</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Rechercher des dépôts…</string>\n    <string name=\"clear_search\">Effacer la recherche</string>\n    <string name=\"filter_all\">Tous</string>\n    <string name=\"filter_with_releases\">Avec versions</string>\n    <string name=\"filter_installed\">Installés</string>\n    <string name=\"filter_favorites\">Favoris</string>\n    <string name=\"sort\">Trier</string>\n    <string name=\"sort_recently_updated\">Récemment mis à jour</string>\n    <string name=\"sort_name\">Nom</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">dépôt</string>\n    <string name=\"repositories_plural\">dépôts</string>\n    <string name=\"showing_x_of_y_repositories\">Affichage de %1$d sur %2$d dépôts</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">Aucun dépôt avec des versions installables</string>\n    <string name=\"no_installed_repos\">Aucun dépôt installé</string>\n    <string name=\"no_favorite_repos\">Aucun dépôt favori</string>\n    <string name=\"report_issue\">Signaler un problème</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">Mis à jour %1$s</string>\n    <string name=\"has_release\">A une version</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">il y a %1$d an(s)</string>\n    <string name=\"time_months_ago\">il y a %1$d mois</string>\n    <string name=\"time_days_ago\">il y a %1$d j</string>\n    <string name=\"time_hours_ago\">il y a %1$d h</string>\n    <string name=\"time_minutes_ago\">il y a %1$d min</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">Publié à l’instant</string>\n    <string name=\"released_hours_ago\">Publié il y a %1$d heure(s)</string>\n    <string name=\"released_yesterday\">Publié hier</string>\n    <string name=\"released_days_ago\">Publié il y a %1$d jour(s)</string>\n    <string name=\"released_on_date\">Publié le %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">Accueil</string>\n    <string name=\"bottom_nav_search_title\">Rechercher</string>\n    <string name=\"bottom_nav_apps_title\">Applications</string>\n    <string name=\"bottom_nav_profile_title\">Profil</string>\n\n    <string name=\"forked_repository\">Fork</string>\n\n    <string name=\"category_stable\">Stable</string>\n    <string name=\"category_pre_release\">Préversion</string>\n    <string name=\"category_all\">Tous</string>\n    <string name=\"select_version\">Sélectionner une version</string>\n    <string name=\"pre_release_badge\">Préversion</string>\n    <string name=\"no_version_selected\">Aucune version sélectionnée</string>\n    <string name=\"versions_title\">Versions</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">Ouvrir le dépôt</string>\n    <string name=\"open_in_browser\">Ouvrir dans le navigateur</string>\n    <string name=\"cancel_download\">Annuler le téléchargement</string>\n    <string name=\"show_install_options\">Afficher les options d\\'installation</string>\n\n    <!-- Errors & states -->\n    <string name=\"no_description\">Aucune description fournie.</string>\n    <string name=\"no_release_notes\">Aucune note de version.</string>\n\n    <!-- Install status -->\n    <string name=\"not_available\">Non disponible</string>\n    <string name=\"update_app\">Mettre à jour</string>\n    <string name=\"pending_install\">Installation en attente</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Ouvrir dans Obtainium</string>\n    <string name=\"obtainium_description\">Gérer les mises à jour automatiquement</string>\n    <string name=\"inspect_with_appmanager\">Inspecter avec AppManager</string>\n    <string name=\"appmanager_description\">Vérifier les permissions, trackers et sécurité</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Désinstaller</string>\n    <string name=\"open_app\">Ouvrir</string>\n    <string name=\"downgrade_requires_uninstall\">La rétrogradation nécessite la désinstallation</string>\n    <string name=\"downgrade_warning_message\">L\\'installation de la version %1$s nécessite la désinstallation de la version actuelle (%2$s). Les données de l\\'application seront perdues.</string>\n    <string name=\"uninstall_first\">Désinstaller d\\'abord</string>\n    <string name=\"install_version\">Installer %1$s</string>\n    <string name=\"failed_to_open_app\">Impossible d\\'ouvrir %1$s</string>\n    <string name=\"failed_to_uninstall\">Impossible de désinstaller %1$s</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">Dernière</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Dernière vérification : %1$s</string>\n    <string name=\"last_checked_never\">Jamais vérifié</string>\n    <string name=\"last_checked_just_now\">à l\\'instant</string>\n    <string name=\"last_checked_minutes_ago\">il y a %1$d min</string>\n    <string name=\"last_checked_hours_ago\">il y a %1$d h</string>\n    <string name=\"checking_for_updates\">Vérification des mises à jour…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Type de proxy</string>\n    <string name=\"proxy_none\">Aucun</string>\n    <string name=\"proxy_system\">Système</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Hôte</string>\n    <string name=\"proxy_port\">Port</string>\n    <string name=\"proxy_username\">Nom d\\'utilisateur (facultatif)</string>\n    <string name=\"proxy_password\">Mot de passe (facultatif)</string>\n    <string name=\"proxy_save\">Sauvegarder le Proxy</string>\n    <string name=\"proxy_saved\">Paramètres du proxy enregistrés</string>\n    <string name=\"proxy_system_description\">Utilise les paramètres proxy de l\\'appareil</string>\n    <string name=\"proxy_port_error\">Le port doit être entre 1 et 65535</string>\n    <string name=\"proxy_none_description\">Connexion directe, pas de proxy</string>\n    <string name=\"failed_to_save_proxy_settings\">Échec de l'enregistrement des paramètres du proxy</string>\n    <string name=\"proxy_host_required\">L’hôte du proxy est requis</string>\n    <string name=\"invalid_proxy_port\">Port proxy invalide</string>\n    <string name=\"proxy_show_password\">Afficher le mot de passe</string>\n    <string name=\"proxy_hide_password\">Masquer le mot de passe</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Suivre cette app</string>\n    <string name=\"app_tracked_successfully\">App ajoutée à la liste de suivi</string>\n    <string name=\"failed_to_track_app\">Échec du suivi de l\\'app : %1$s</string>\n    <string name=\"already_tracked\">L\\'app est déjà suivie</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">Se connecter à GitHub</string>\n    <string name=\"profile_sign_in_description\">Débloquez l\\'expérience complète. Gérez vos apps, synchronisez vos préférences et naviguez plus vite.</string>\n    <string name=\"profile_repos\">Dépôts</string>\n    <string name=\"profile_login\">Connexion</string>\n    <string name=\"profile_stars_description\">Vos dépôts étoilés sur GitHub</string>\n    <string name=\"profile_favourites_description\">Vos dépôts favoris enregistrés localement</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Session expirée</string>\n    <string name=\"session_expired_message\">Votre session GitHub a expiré ou le jeton a été révoqué. Veuillez vous reconnecter pour continuer à utiliser les fonctionnalités authentifiées.</string>\n    <string name=\"session_expired_hint\">Vous pouvez toujours naviguer en tant qu\\'invité avec des requêtes API limitées.</string>\n    <string name=\"sign_in_again\">Se reconnecter</string>\n    <string name=\"continue_as_guest\">Continuer en tant qu\\'invité</string>\n    <string name=\"logout_revocation_note\">Cela effacera votre session locale et les données en cache. Pour révoquer complètement l\\'accès, visitez GitHub Settings > Applications.</string>\n    <string name=\"auth_code_expires_in\">Le code expire dans %1$s</string>\n    <string name=\"auth_error_code_expired\">Le code de l\\'appareil a expiré.</string>\n    <string name=\"auth_hint_try_again\">Veuillez réessayer de vous connecter pour obtenir un nouveau code.</string>\n    <string name=\"auth_hint_check_connection\">Vérifiez votre connexion internet et réessayez.</string>\n    <string name=\"auth_hint_denied\">Vous avez refusé la demande d\\'autorisation. Réessayez si c\\'était involontaire.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Lire la suite</string>\n    <string name=\"show_less\">Afficher moins</string>\n\n    <string name=\"share_repository\">Partager le dépôt</string>\n    <string name=\"failed_to_share_link\">Échec du partage du lien</string>\n    <string name=\"link_copied_to_clipboard\">Lien copié dans le presse-papiers</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Traduire</string>\n    <string name=\"translating\">Traduction…</string>\n    <string name=\"show_original\">Afficher l\\'original</string>\n    <string name=\"translated_to\">Traduit en %1$s</string>\n    <string name=\"translate_to\">Traduire en…</string>\n    <string name=\"search_language\">Rechercher une langue</string>\n    <string name=\"change_language\">Changer de langue</string>\n    <string name=\"translation_failed\">Échec de la traduction. Veuillez réessayer.</string>\n\n    <string name=\"open_github_link\">Ouvrir le lien GitHub</string>\n    <string name=\"clipboard_link_detected\">Lien GitHub détecté dans le presse-papiers</string>\n    <string name=\"auto_detect_clipboard_links\">Détecter les liens du presse-papiers</string>\n    <string name=\"auto_detect_clipboard_description\">Détecter automatiquement les liens GitHub du presse-papiers lors de l\\'ouverture de la recherche</string>\n    <string name=\"detected_links\">Liens détectés</string>\n    <string name=\"open_in_app\">Ouvrir dans l\\'app</string>\n    <string name=\"no_github_link_in_clipboard\">Aucun lien GitHub trouvé dans le presse-papiers</string>\n\n    <string name=\"storage\">Stockage</string>\n    <string name=\"clear_cache\">Vider le cache</string>\n    <string name=\"current_size\">Taille actuelle :</string>\n    <string name=\"clear\">Vider</string>\n\n    <string name=\"sponsor_title\">Soutenir GitHub Store</string>\n    <string name=\"sponsor_button\">Soutenir le projet</string>\n    <string name=\"sponsor_hero_title\">Créé avec amour,\\nmaintenu avec du café</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store a dépassé 130 000 téléchargements et 7 700 étoiles GitHub — 100 % gratuit, sans publicité ni suivi.</string>\n    <string name=\"sponsor_personal_note\">J’ai construit et je maintiens ce projet seul tout en terminant le lycée. Votre soutien — même petit — aide à garder l’application sans bugs, payer l’infrastructure et livrer les fonctionnalités demandées.</string>\n\n    <string name=\"sponsor_kodee_title\">Votez pour GitHub Store !</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store est nommé aux Golden Kodee Awards de KotlinConf 2026. Votre vote prend seulement 2 minutes.</string>\n\n    <string name=\"sponsor_kodee_register\">1. S’inscrire</string>\n    <string name=\"sponsor_kodee_vote\">2. Voter</string>\n    <string name=\"sponsor_kodee_deadline\">Vote jusqu’au 22 mars</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Inscrivez-vous sur la plateforme (Continuer avec Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. Appuyez sur Voter ci-dessous</string>\n    <string name=\"sponsor_kodee_step3\">3. Trouvez Usmon Narzullayev et cliquez sur Voter</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">Mensuel ou ponctuel via GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Soutien rapide en une fois</string>\n\n    <string name=\"sponsor_other_ways_title\">AUTRES FAÇONS D’AIDER</string>\n\n    <string name=\"sponsor_star_repo\">Mettre une étoile au dépôt</string>\n    <string name=\"sponsor_star_repo_desc\">Aide d’autres personnes à découvrir l’app</string>\n\n    <string name=\"sponsor_report_bugs\">Signaler des bugs</string>\n    <string name=\"sponsor_report_bugs_desc\">Améliore l’application pour tous</string>\n\n    <string name=\"sponsor_share\">Partager avec des amis</string>\n    <string name=\"sponsor_share_desc\">Parlez-en aux développeurs</string>\n\n    <string name=\"sponsor_thank_you\">Chaque soutien — financier ou non — maintient ce projet en vie. Merci !</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Installation</string>\n    <string name=\"installer_type_default\">Par défaut</string>\n    <string name=\"installer_type_default_description\">Boîte de dialogue d\\'installation système standard</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Installation silencieuse sans confirmation</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku n\\'est pas installé</string>\n    <string name=\"shizuku_status_not_running\">Shizuku n\\'est pas en cours d\\'exécution</string>\n    <string name=\"shizuku_status_permission_needed\">Autorisation requise</string>\n    <string name=\"shizuku_status_ready\">Prêt</string>\n    <string name=\"shizuku_grant_permission\">Accorder l\\'autorisation</string>\n    <string name=\"shizuku_install_hint\">Installez Shizuku pour activer l\\'installation silencieuse</string>\n    <string name=\"shizuku_start_hint\">Démarrez Shizuku pour activer l\\'installation silencieuse</string>\n    <string name=\"shizuku_install_failed_fallback\">L\\'installation via Shizuku a échoué, utilisation de l\\'installateur standard</string>\n\n    <string name=\"auto_update_title\">Mise à jour automatique</string>\n    <string name=\"auto_update_description\">Télécharger et installer automatiquement les mises à jour en arrière-plan via Shizuku</string>\n\n    <string name=\"section_updates\">Mises à jour</string>\n    <string name=\"update_check_interval_title\">Intervalle de vérification</string>\n    <string name=\"update_check_interval_description\">Fréquence de vérification des mises à jour en arrière-plan</string>\n    <string name=\"interval_3h\">3h</string>\n    <string name=\"interval_6h\">6h</string>\n    <string name=\"interval_12h\">12h</string>\n    <string name=\"interval_24h\">24h</string>\n\n    <string name=\"add_by_link\">Ajouter par lien</string>\n    <string name=\"link_app_title\">Lier l\\'app au dépôt</string>\n    <string name=\"pick_installed_app\">Choisissez une app installée à lier à un dépôt GitHub</string>\n    <string name=\"search_apps_hint\">Rechercher des apps…</string>\n    <string name=\"enter_repo_url\">URL du dépôt GitHub</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Validation…</string>\n    <string name=\"link_and_track\">Lier et suivre</string>\n    <string name=\"checking_release\">Vérification de la dernière version…</string>\n    <string name=\"downloading_for_verification\">Téléchargement de l\\'APK pour vérification…</string>\n    <string name=\"verifying_signing_key\">Vérification de la clé de signature…</string>\n    <string name=\"package_name_mismatch\">Nom de paquet différent : l\\'APK est %1$s, mais l\\'app sélectionnée est %2$s</string>\n    <string name=\"signing_key_mismatch_link\">Clé de signature différente : l\\'APK de ce dépôt a été signé par un autre développeur</string>\n    <string name=\"select_asset_title\">Sélectionner l\\'installateur</string>\n    <string name=\"select_asset_description\">Choisissez l\\'APK à vérifier avec votre app installée</string>\n    <string name=\"download_failed\">Échec du téléchargement</string>\n    <string name=\"export_apps\">Exporter</string>\n    <string name=\"import_apps\">Importer</string>\n    <string name=\"import_apps_title\">Importer des apps</string>\n    <string name=\"import_apps_description\">Collez le JSON exporté pour restaurer vos apps suivies</string>\n    <string name=\"import_apps_hint\">Collez le JSON exporté ici…</string>\n    <string name=\"include_pre_releases_title\">Inclure les pré-versions</string>\n    <string name=\"include_pre_releases_description\">Suivre les versions pré-release lors de la vérification des mises à jour. Désactivé, seules les versions stables sont prises en compte.</string>\n    <string name=\"confirm_uninstall_title\">Désinstaller l\\'app ?</string>\n    <string name=\"confirm_uninstall_message\">Êtes-vous sûr de vouloir désinstaller %1$s ? Cette action est irréversible et les données de l\\'app pourraient être perdues.</string>\n    <string name=\"invalid_github_url\">URL GitHub invalide. Utilisez le format : github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Dépôt introuvable : %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">Limite de l\\'API GitHub dépassée. Réessayez plus tard.</string>\n    <string name=\"failed_to_link\">Échec de la liaison : %1$s</string>\n    <string name=\"failed_to_load_apps\">Échec du chargement des apps installées</string>\n    <string name=\"app_linked_success\">%1$s liée à %2$s/%3$s</string>\n    <string name=\"export_failed\">Échec de l\\'exportation : %1$s</string>\n    <string name=\"import_failed\">Échec de l\\'importation : %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d apps importées</string>\n    <string name=\"imported_skipped\">, %1$d ignorées</string>\n    <string name=\"imported_failed\">, %1$d échouées</string>\n    <string name=\"signing_key_changed_title\">Clé de signature modifiée</string>\n    <string name=\"signing_key_changed_message\">Le certificat de signature de cette app a changé depuis sa première installation.\\n\\nCela peut signifier que le développeur a changé sa clé de signature, ou que le binaire a été altéré.\\n\\nAttendu : %1$s\\nReçu : %2$s</string>\n    <string name=\"install_anyway\">Installer quand même</string>\n    <string name=\"verified_build\">Build vérifié</string>\n    <string name=\"checking_attestation\">Vérification\\u2026</string>\n    <string name=\"assets_title\">Ressources</string>\n    <string name=\"no_assets_selected\">Aucune ressource</string>\n    <string name=\"no_assets_in_list\">Aucune ressource associée à cette version</string>\n    <string name=\"assets_selection_label\">Sélectionner une option de ressource</string>\n    <string name=\"multiple_assets_info_dialog_title\">Plusieurs ressources disponibles</string>\n    <string name=\"multiple_assets_info_dialog_text\">Plusieurs fichiers installables sont disponibles pour cette version. Veuillez examiner la liste et sélectionner celui qui correspond à votre appareil.</string>\n    <string name=\"icon_content_description_info\">Informations</string>\n    <string name=\"translation_error_retry\">Réessayer</string>\n    <string name=\"translated_from\">Détection automatique : %1$s</string>\n    <string name=\"select_language\">Sélectionner la langue</string>\n    <string name=\"update_package_mismatch\">Incompatibilité de paquet : l\\'APK est %1$s, mais l\\'application installée est %2$s. Mise à jour bloquée.</string>\n    <string name=\"update_signing_key_mismatch\">Incompatibilité de clé de signature : la mise à jour a été signée par un développeur différent. Mise à jour bloquée.</string>\n    <string name=\"liquid_glass_option_title\">Effet verre liquide</string>\n    <string name=\"liquid_glass_option_description\">Améliorez l\\'interface avec une apparence vitreuse et fluide</string>\n\n    <string name=\"hide_seen_title\">Masquer les dépôts consultés</string>\n    <string name=\"hide_seen_description\">Masquer les dépôts que vous avez déjà consultés des flux de découverte</string>\n    <string name=\"clear_seen_history\">Effacer l\\'historique des consultations</string>\n    <string name=\"clear_seen_history_description\">Réinitialiser tous les dépôts consultés pour qu\\'ils réapparaissent dans les flux</string>\n    <string name=\"seen_history_cleared\">Historique des consultations effacé</string>\n    <string name=\"seen_badge\">Consulté</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">इंस्टॉल किए गए ऐप्स</string>\n    <string name=\"navigate_back\">वापस जाएँ</string>\n    <string name=\"check_for_updates\">अपडेट के लिए जाँच करें</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">%1$s को लॉन्च नहीं किया जा सकता</string>\n    <string name=\"failed_to_open\">%1$s को खोलने में विफल रहा</string>\n    <string name=\"failed_to_update\">%1$s अपडेट करने में विफल रहा: %2$s</string>\n    <string name=\"update_failed\">अपडेट विफल</string>\n    <string name=\"update_all_failed\">सभी अपडेट विफल रहे: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">सभी ऐप्स सफलतापूर्वक अपडेट हो गए</string>\n    <string name=\"no_updates_available\">कोई अपडेट उपलब्ध नहीं</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">अपने ऐप्स खोजें</string>\n    <string name=\"no_apps_found\">कोई ऐप नहीं मिला</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">सभी अपडेट करें</string>\n    <string name=\"update\">अपडेट</string>\n    <string name=\"open\">खोलें</string>\n    <string name=\"cancel\">रद्द करें</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">जाँच हो रही है...</string>\n    <string name=\"updated_successfully\">सफलतापूर्वक अपडेट किया गया</string>\n    <string name=\"error_with_message\">गलती: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">%2$d में से %1$d अपडेट हो रहा है</string>\n    <string name=\"currently_updating\">वर्तमान में: %1$s</string>\n\n\n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">अनुमति की प्रतीक्षा की जा रही है...</string>\n    <string name=\"signed_in\">साइन इन किया गया!</string>\n    <string name=\"redirecting_message\">अब आप ऐप का इस्तेमाल कर सकते हैं। रीडायरेक्ट किया जा रहा है…</string>\n    <string name=\"try_again\">पुनः प्रयास करें</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">गलती: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">यह कोड GitHub पर डालें:</string>\n    <string name=\"copy_code\">कोड कॉपी करें</string>\n    <string name=\"open_github\">GitHub खोलें</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">पूरा अनुभव अनलॉक\\nकरें</string>\n\n    <string name=\"more_requests\">और अनुरोध</string>\n    <string name=\"more_requests_description\">\n        ज़्यादा API रेट लिमिट पाने और रुकावटों से बचने के लिए साइन इन करें।\n    </string>\n\n    <string name=\"sign_in_with_github\">GitHub से साइन इन करें</string>\n\n    <string name=\"error_cancelled\">रद्द किया गया</string>\n    <string name=\"error_unknown\">अज्ञात त्रुटि</string>\n\n    <string name=\"language_label\">भाषा:</string>\n\n    <string name=\"discover_repositories\">रिपॉजिटरीज़ खोजें</string>\n    <string name=\"search_repositories_hint\">रेपो, विवरण खोजें…</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">भाषा के अनुसार फ़िल्टर करें</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">%1$d परिणाम मिले</string>\n\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">क्रमबद्ध करें</string>\n    <string name=\"close\">बंद</string>\n\n    <string name=\"sort_most_stars\">सबसे अधिक स्टार</string>\n    <string name=\"sort_most_forks\">सबसे अधिक फोर्क्स</string>\n    <string name=\"sort_best_match\">सबसे अच्छा मिलान</string>\n\n    <string name=\"sort_order_descending\">अवरोही</string>\n    <string name=\"sort_order_ascending\">आरोही</string>\n    <string name=\"sort_label\">क्रमबद्ध करें</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">सभी भाषाएँ</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">खोज विफल रही</string>\n    <string name=\"no_repositories_found\">कोई रिपॉजिटरी नहीं मिली</string>\n\n    <!-- Settings -->\n    <string name=\"profile_title\">प्रोफ़ाइल</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">उपस्थिति</string>\n    <string name=\"section_about\">के बारे में</string>\n    <string name=\"section_network\">नेटवर्क</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">थीम रंग</string>\n    <string name=\"amoled_black_theme\">AMOLED ब्लैक थीम</string>\n    <string name=\"amoled_black_description\">डार्क मोड के लिए गहरा काला बैकग्राउंड</string>\n    <string name=\"selected_color\">चुना हुआ रंग: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">संस्करण</string>\n    <string name=\"help_support\">सहायता &amp; समर्थन</string>\n\n    <!-- Account -->\n    <string name=\"logout\">लॉग आउट</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">सफलतापूर्वक लॉग आउट हो गए, रीडायरेक्ट किया जा रहा है...</string>\n    <string name=\"cache_cleared\">कैश सफलतापूर्वक साफ़ किया गया</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">चेतावनी!</string>\n    <string name=\"logout_confirmation\">क्या आप लॉग आउट करना चाहते हैं?</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">डायनामिक</string>\n    <string name=\"theme_ocean\">ओशन</string>\n    <string name=\"theme_purple\">पर्पल</string>\n    <string name=\"theme_forest\">फॉरेस्ट</string>\n    <string name=\"theme_slate\">स्लेट</string>\n    <string name=\"theme_amber\">एम्बर</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">रिपॉजिटरी खोलें</string>\n    <string name=\"open_in_browser\">ब्राउज़र में खोलें</string>\n    <string name=\"cancel_download\">डाउनलोड रद्द करें</string>\n    <string name=\"show_install_options\">इंस्टॉल विकल्प दिखाएँ</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">विवरण लोड करने में त्रुटि</string>\n    <string name=\"retry\">पुन: प्रयास करें</string>\n    <string name=\"no_description\">कोई विवरण नहीं दिया गया है।</string>\n    <string name=\"no_release_notes\">कोई रिलीज़ नोट्स नहीं।</string>\n    <string name=\"report_issue\">समस्या की रिपोर्ट करें</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">इस ऐप के बारे में</string>\n    <string name=\"install_logs\">इंस्टॉल लॉग्स</string>\n    <string name=\"author\">लेखक</string>\n    <string name=\"whats_new\">नया क्या है</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">स्थापित</string>\n    <string name=\"update_available\">उपलब्ध अपडेट</string>\n    <string name=\"not_available\">उपलब्ध नहीं है</string>\n    <string name=\"install_latest\">नवीनतम स्थापित करें</string>\n    <string name=\"reinstall\">पुनः इंस्टॉल करें</string>\n    <string name=\"update_app\">ऐप अपडेट करें</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">डाउनलोड हो रहा है</string>\n    <string name=\"updating\">अपडेट हो रहा है</string>\n    <string name=\"verifying\">सत्यापन हो रहा है</string>\n    <string name=\"installing\">स्थापित हो रहा है</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">ओब्टेनियम में खोलें</string>\n    <string name=\"obtainium_description\">अपडेट को अपने आप मैनेज करें</string>\n    <string name=\"inspect_with_appmanager\">ऐपमैनेजर से जांच करें</string>\n    <string name=\"appmanager_description\">अनुमतियाँ, ट्रैकर्स जाँचें &amp; सुरक्षा</string>\n\n    <!-- Author -->\n    <string name=\"profile\">प्रोफ़ाइल</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">फोर्क्स</string>\n    <string name=\"stars\">स्टार्स</string>\n    <string name=\"issues\">इश्यूज़</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">द्वारा %1$s</string>\n    <string name=\"installed_version\">• स्थापित: %1$s</string>\n    <string name=\"architecture_compatible\">आर्किटेक्चर संगत</string>\n    <string name=\"update_to_version\">%1$s पर अपडेट करें</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">विवरण लोड करने में विफल</string>\n    <string name=\"installer_saved_downloads\">इंस्टॉलर को डाउनलोड फ़ोल्डर में सेव कर दिया गया है।</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">डाउनलोड शुरू हो गया</string>\n    <string name=\"log_downloaded\">डाउनलोड हुआ</string>\n    <string name=\"log_update_started\">अपडेट शुरू हो गया</string>\n    <string name=\"log_installed\">स्थापित</string>\n    <string name=\"log_updated\">अद्यतन</string>\n    <string name=\"log_cancelled\">रद्द किया गया</string>\n    <string name=\"log_install_started\">इंस्टॉलेशन शुरू हो गया</string>\n    <string name=\"log_error\">गलती</string>\n    <string name=\"log_error_with_message\">गलती: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">ऐपमैनेजर के लिए तैयारी कर रहा है</string>\n    <string name=\"log_opened_appmanager\">ऐप मैनेजर में खोला गया</string>\n    <string name=\"log_permission_blocked\">डिवाइस नीति द्वारा इंस्टॉल अनुमति अवरुद्ध</string>\n    <string name=\"log_opened_external_installer\">बाहरी इंस्टॉलर में खोला गया</string>\n    <string name=\"install_permission_unavailable\">इंस्टॉल अनुमति उपलब्ध नहीं</string>\n    <string name=\"install_permission_blocked_message\">APK सफलतापूर्वक डाउनलोड हो गया लेकिन यह डिवाइस सीधे इंस्टॉल करने की अनुमति नहीं देता। क्या आप इसे बाहरी इंस्टॉलर से खोलना चाहेंगे?</string>\n    <string name=\"open_with_external_installer\">बाहरी इंस्टॉलर से खोलें</string>\n    <string name=\"external_installer_description\">APK इंस्टॉल करने के लिए तृतीय-पक्ष ऐप का उपयोग करें</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">गलती: %1$s</string>\n    <string name=\"error_asset_not_supported\">संपदा प्रकार .%1$s समर्थित नहीं</string>\n    <string name=\"error_file_not_found\">डाउनलोड की गई फ़ाइल नहीं मिली</string>\n\n    <string name=\"home_category_trending\">रुझान</string>\n\n    <string name=\"home_finding_repositories\">रिपॉजिटरी ढूंढी जा रही हैं...</string>\n    <string name=\"home_loading_more\">और लोड हो रहा है...</string>\n    <string name=\"home_no_more_repositories\">अब और कोई रिपॉजिटरी नहीं</string>\n    <string name=\"home_retry\">पुन: प्रयास करें</string>\n    <string name=\"home_failed_to_load_repositories\">रिपॉजिटरी लोड करने में विफल रहा</string>\n    <string name=\"home_view_details\">विवरण देखें</string>\n\n    <string name=\"updated_just_now\">अभी-अभी अपडेट किया गया</string>\n    <string name=\"updated_hours_ago\">%1$d घंटे पहले अपडेट किया गया</string>\n    <string name=\"updated_yesterday\">कल अपडेट किया गया</string>\n    <string name=\"updated_days_ago\">%1$d दिन पहले अपडेट किया गया</string>\n    <string name=\"updated_on_date\">%1$s को अपडेट किया गया</string>\n\n    <string name=\"rate_limit_exceeded\">दर सीमा पार हो गई</string>\n    <string name=\"rate_limit_used_all\">आपने सभी %1$d API रिक्वेस्ट का इस्तेमाल कर लिया है।</string>\n    <string name=\"rate_limit_used_all_free\">आपने सभी %1$d फ्री API रिक्वेस्ट इस्तेमाल कर लिए हैं।</string>\n    <string name=\"rate_limit_resets_in_minutes\">%1$d मिनट में रीसेट हो जाएगा</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 60 रिक्वेस्ट प्रति घंटे के बजाय 5,000 रिक्वेस्ट प्रति घंटे पाने के लिए साइन इन करें!</string>\n    <string name=\"rate_limit_sign_in\">साइन इन</string>\n    <string name=\"rate_limit_ok\">ठीक है</string>\n    <string name=\"rate_limit_close\">बंद करें</string>\n\n    <string name=\"system_font\">सिस्टम फ़ॉन्ट</string>\n    <string name=\"system_font_description\">बेहतर पठनीयता के लिए अपने डिवाइस के फ़ॉन्ट से मिलान करें।</string>\n\n    <string name=\"theme_light\">हल्का</string>\n    <string name=\"theme_dark\">डार्क</string>\n    <string name=\"theme_system\">सिस्टम</string>\n\n    <string name=\"added_to_favourites\">रिपॉजिटरी को पसंदीदा में जोड़ा गया</string>\n    <string name=\"removed_from_favourites\">रिपॉजिटरी को पसंदीदा से हटा दिया गया</string>\n    <string name=\"add_to_favourites\">पसंदीदा में जोड़ें</string>\n    <string name=\"remove_from_favourites\">पसंदीदा से निकालें</string>\n    <string name=\"favourites\">पसंदीदा</string>\n\n    <string name=\"added_just_now\">अभी जोड़ा गया</string>\n    <string name=\"added_hours_ago\">%1$d घंटे पहले जोड़ा गया</string>\n    <string name=\"added_yesterday\">कल जोड़ा गया</string>\n    <string name=\"added_days_ago\">%1$d दिन पहले जोड़ा गया</string>\n    <string name=\"added_on_date\">%1$s को जोड़ा गया</string>\n\n    <string name=\"starred_repositories\">तारांकित रिपॉजिटरी</string>\n    <string name=\"repository_starred\">रिपॉजिटरी स्टार की गई</string>\n    <string name=\"repository_not_starred\">रिपॉजिटरी स्टार नहीं की गई</string>\n    <string name=\"star_from_github\">आप GitHub से किसी रिपॉजिटरी को स्टार कर सकते हैं।</string>\n    <string name=\"unstar_from_github\">आप GitHub से किसी रिपॉजिटरी को अनस्टार कर सकते हैं।</string>\n\n    <string name=\"sign_in_required\">साइन इन आवश्यक</string>\n    <string name=\"sign_in_with_github_for_stars\">अपने स्टार किए गए रिपॉजिटरी देखने के लिए GitHub से साइन इन करें</string>\n    <string name=\"no_starred_repos\">कोई तारांकित रिपॉजिटरी नहीं</string>\n    <string name=\"star_repos_hint\">उन्हें यहां देखने के लिए इंस्टॉलेबल रिलीज़ वाले GitHub पर रिपॉजिटरी को स्टार करें।</string>\n    <string name=\"last_synced\">अंतिम सिंक</string>\n    <string name=\"just_now\">अभी-अभी</string>\n    <string name=\"minutes_ago\">%1$d मिनट पहले</string>\n    <string name=\"hours_ago\">%1$d घंटे पहले</string>\n    <string name=\"days_ago\">%1$d दिन पहले</string>\n    <string name=\"dismiss\">हटाएं</string>\n    <string name=\"sync_starred_failed\">तारांकित रिपॉजिटरी को सिंक करने में विफल रहा</string>\n\n    <string name=\"developer_profile_title\">डेवलपर प्रोफ़ाइल</string>\n    <string name=\"open_developer_profile\">डेवलपर प्रोफ़ाइल खोलें</string>\n\n    <string name=\"failed_to_load_repositories\">रिपॉजिटरी लोड करने में विफल रहा</string>\n    <string name=\"failed_to_load_profile\">प्रोफ़ाइल लोड करने में विफल</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">रिपॉजिटरी</string>\n    <string name=\"followers\">फ़ॉलोअर्स</string>\n    <string name=\"following\">फ़ॉलो कर रहे हैं</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">रिपॉजिटरी खोजें…</string>\n    <string name=\"clear_search\">खोज साफ़ करें</string>\n    <string name=\"filter_all\">सभी</string>\n    <string name=\"filter_with_releases\">रिलीज़ के साथ</string>\n    <string name=\"filter_installed\">स्थापित</string>\n    <string name=\"filter_favorites\">पसंदीदा</string>\n    <string name=\"sort\">क्रम</string>\n    <string name=\"sort_recently_updated\">हाल ही में अपडेट किया गया</string>\n    <string name=\"sort_name\">नाम</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">रिपॉजिटरी</string>\n    <string name=\"repositories_plural\">रिपॉजिटरीज़</string>\n    <string name=\"showing_x_of_y_repositories\">%2$d में से %1$d रिपॉजिटरी दिखाए जा रहे हैं</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">इंस्टॉल करने योग्य रिलीज़ वाली कोई रिपॉजिटरी नहीं</string>\n    <string name=\"no_installed_repos\">कोई रिपॉजिटरी इंस्टॉल नहीं है</string>\n    <string name=\"no_favorite_repos\">कोई पसंदीदा रिपॉजिटरी नहीं</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">%1$s पहले अपडेट किया गया</string>\n    <string name=\"has_release\">रिलीज़ हो गया है</string>\n\n    <string name=\"time_years_ago\">%1$d साल पहले</string>\n    <string name=\"time_months_ago\">%1$d महीने पहले</string>\n    <string name=\"time_days_ago\">%1$d दिन पहले</string>\n    <string name=\"time_hours_ago\">%1$d घंटे पहले</string>\n    <string name=\"time_minutes_ago\">%1$d मिनट पहले</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">अभी-अभी जारी किया गया</string>\n    <string name=\"released_hours_ago\">%1$d घंटे पहले जारी किया गया</string>\n    <string name=\"released_yesterday\">कल जारी किया गया</string>\n    <string name=\"released_days_ago\">%1$d दिन पहले जारी किया गया</string>\n    <string name=\"released_on_date\">%1$s को जारी किया गया</string>\n\n    <string name=\"bottom_nav_home_title\">होम</string>\n    <string name=\"bottom_nav_search_title\">खोज</string>\n    <string name=\"bottom_nav_apps_title\">ऐप्स</string>\n    <string name=\"bottom_nav_profile_title\">प्रोफ़ाइल</string>\n\n    <string name=\"forked_repository\">फोर्क</string>\n\n    <string name=\"category_stable\">स्थिर</string>\n    <string name=\"category_pre_release\">प्री-रिलीज़</string>\n    <string name=\"category_all\">सभी</string>\n    <string name=\"select_version\">संस्करण चुनें</string>\n    <string name=\"pre_release_badge\">प्री-रिलीज़</string>\n    <string name=\"no_version_selected\">कोई संस्करण चयनित नहीं</string>\n    <string name=\"versions_title\">संस्करण</string>\n\n    <!-- Home categories -->\n    <string name=\"home_category_hot_release\">हॉट रिलीज़</string>\n    <string name=\"home_category_most_popular\">सबसे लोकप्रिय</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">इंस्टॉल लंबित</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">अनइंस्टॉल</string>\n    <string name=\"open_app\">खोलें</string>\n    <string name=\"downgrade_requires_uninstall\">डाउनग्रेड के लिए अनइंस्टॉल आवश्यक</string>\n    <string name=\"downgrade_warning_message\">संस्करण %1$s इंस्टॉल करने के लिए पहले वर्तमान संस्करण (%2$s) को अनइंस्टॉल करना होगा। ऐप डेटा खो जाएगा।</string>\n    <string name=\"uninstall_first\">पहले अनइंस्टॉल करें</string>\n    <string name=\"install_version\">%1$s इंस्टॉल करें</string>\n    <string name=\"failed_to_open_app\">%1$s खोलने में विफल</string>\n    <string name=\"failed_to_uninstall\">%1$s अनइंस्टॉल करने में विफल</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">नवीनतम</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">अंतिम जाँच: %1$s</string>\n    <string name=\"last_checked_never\">कभी जाँच नहीं की</string>\n    <string name=\"last_checked_just_now\">अभी</string>\n    <string name=\"last_checked_minutes_ago\">%1$d मिनट पहले</string>\n    <string name=\"last_checked_hours_ago\">%1$d घंटे पहले</string>\n    <string name=\"checking_for_updates\">अपडेट की जाँच हो रही है…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">प्रॉक्सी प्रकार</string>\n    <string name=\"proxy_none\">कोई नहीं</string>\n    <string name=\"proxy_system\">सिस्टम</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">होस्ट</string>\n    <string name=\"proxy_port\">पोर्ट</string>\n    <string name=\"proxy_username\">उपयोगकर्ता नाम (वैकल्पिक)</string>\n    <string name=\"proxy_password\">पासवर्ड (वैकल्पिक)</string>\n    <string name=\"proxy_save\">प्रॉक्सी सहेजें</string>\n    <string name=\"proxy_saved\">प्रॉक्सी सेटिंग्स सहेजी गईं</string>\n    <string name=\"proxy_system_description\">आपके डिवाइस की प्रॉक्सी सेटिंग का उपयोग करता है</string>\n    <string name=\"proxy_port_error\">पोर्ट 1–65535 के बीच होना चाहिए</string>\n    <string name=\"proxy_none_description\">सीधा कनेक्शन, कोई प्रॉक्सी नहीं</string>\n    <string name=\"failed_to_save_proxy_settings\">प्रॉक्सी सेटिंग्स सहेजने में विफल</string>\n    <string name=\"proxy_host_required\">प्रॉक्सी होस्ट आवश्यक है</string>\n    <string name=\"invalid_proxy_port\">अमान्य प्रॉक्सी पोर्ट</string>\n    <string name=\"proxy_show_password\">पासवर्ड दिखाएँ</string>\n    <string name=\"proxy_hide_password\">पासवर्ड छुपाएँ</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">इस ऐप को ट्रैक करें</string>\n    <string name=\"app_tracked_successfully\">ऐप ट्रैकिंग सूची में जोड़ा गया</string>\n    <string name=\"failed_to_track_app\">ऐप ट्रैक करने में विफल: %1$s</string>\n    <string name=\"already_tracked\">ऐप पहले से ट्रैक हो रहा है</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">GitHub में साइन इन करें</string>\n    <string name=\"profile_sign_in_description\">पूरा अनुभव अनलॉक करें। अपने ऐप्स मैनेज करें, प्राथमिकताएँ सिंक करें और तेज़ी से ब्राउज़ करें।</string>\n    <string name=\"profile_repos\">रिपॉजिटरी</string>\n    <string name=\"profile_login\">लॉगिन</string>\n    <string name=\"profile_stars_description\">GitHub पर आपकी स्टार की गई रिपॉजिटरी</string>\n    <string name=\"profile_favourites_description\">स्थानीय रूप से सहेजी गई आपकी पसंदीदा रिपॉजिटरी</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">सत्र समाप्त</string>\n    <string name=\"session_expired_message\">आपका GitHub सत्र समाप्त हो गया है या टोकन रद्द कर दिया गया है। प्रमाणित सुविधाओं का उपयोग जारी रखने के लिए कृपया फिर से साइन इन करें।</string>\n    <string name=\"session_expired_hint\">आप सीमित API अनुरोधों के साथ अतिथि के रूप में ब्राउज़ कर सकते हैं।</string>\n    <string name=\"sign_in_again\">फिर से साइन इन करें</string>\n    <string name=\"continue_as_guest\">अतिथि के रूप में जारी रखें</string>\n    <string name=\"logout_revocation_note\">यह आपका स्थानीय सत्र और कैश डेटा साफ़ कर देगा। पूर्ण रूप से पहुँच रद्द करने के लिए, GitHub Settings > Applications पर जाएँ।</string>\n    <string name=\"auth_code_expires_in\">कोड %1$s में समाप्त होगा</string>\n    <string name=\"auth_error_code_expired\">डिवाइस कोड की अवधि समाप्त हो गई है।</string>\n    <string name=\"auth_hint_try_again\">नया कोड प्राप्त करने के लिए कृपया फिर से साइन इन करें।</string>\n    <string name=\"auth_hint_check_connection\">कृपया अपना इंटरनेट कनेक्शन जाँचें और पुनः प्रयास करें।</string>\n    <string name=\"auth_hint_denied\">आपने प्राधिकरण अनुरोध अस्वीकार कर दिया। यदि यह अनजाने में हुआ तो पुनः प्रयास करें।</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">और पढ़ें</string>\n    <string name=\"show_less\">कम दिखाएं</string>\n\n    <string name=\"share_repository\">रिपॉजिटरी साझा करें</string>\n    <string name=\"failed_to_share_link\">लिंक साझा करने में विफल</string>\n    <string name=\"link_copied_to_clipboard\">लिंक क्लिपबोर्ड में कॉपी किया गया</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">अनुवाद करें</string>\n    <string name=\"translating\">अनुवाद हो रहा है…</string>\n    <string name=\"show_original\">मूल दिखाएं</string>\n    <string name=\"translated_to\">%1$s में अनुवादित</string>\n    <string name=\"translate_to\">अनुवाद करें…</string>\n    <string name=\"search_language\">भाषा खोजें</string>\n    <string name=\"change_language\">भाषा बदलें</string>\n    <string name=\"translation_failed\">अनुवाद विफल। कृपया पुनः प्रयास करें।</string>\n\n    <string name=\"open_github_link\">GitHub लिंक खोलें</string>\n    <string name=\"clipboard_link_detected\">क्लिपबोर्ड में GitHub लिंक मिला</string>\n    <string name=\"auto_detect_clipboard_links\">क्लिपबोर्ड लिंक स्वतः पहचानें</string>\n    <string name=\"auto_detect_clipboard_description\">खोज खोलते समय क्लिपबोर्ड से GitHub लिंक स्वचालित रूप से पहचानें</string>\n    <string name=\"detected_links\">पहचाने गए लिंक</string>\n    <string name=\"open_in_app\">ऐप में खोलें</string>\n    <string name=\"no_github_link_in_clipboard\">क्लिपबोर्ड में कोई GitHub लिंक नहीं मिला</string>\n\n    <string name=\"storage\">संग्रहण</string>\n    <string name=\"clear_cache\">कैश साफ़ करें</string>\n    <string name=\"current_size\">वर्तमान आकार:</string>\n    <string name=\"clear\">साफ़ करें</string>\n\n    <string name=\"sponsor_title\">GitHub Store का समर्थन करें</string>\n    <string name=\"sponsor_button\">प्रोजेक्ट को समर्थन दें</string>\n    <string name=\"sponsor_hero_title\">प्यार से बनाया,\\nकॉफी से चलाया</string>\n\n    <string name=\"sponsor_hero_subtitle\">GitHub Store ने 130,000+ डाउनलोड और 7,700+ GitHub स्टार प्राप्त किए हैं — 100% मुफ्त, बिना विज्ञापन और बिना ट्रैकिंग।</string>\n\n    <string name=\"sponsor_personal_note\">मैं इस प्रोजेक्ट को पूरी तरह अकेले बनाता और संभालता हूँ जबकि मैं हाई स्कूल पूरा कर रहा हूँ।</string>\n\n    <string name=\"sponsor_kodee_title\">GitHub Store के लिए वोट करें!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store KotlinConf 2026 Golden Kodee Awards के लिए नामांकित है।</string>\n\n    <string name=\"sponsor_kodee_register\">1. रजिस्टर करें</string>\n    <string name=\"sponsor_kodee_vote\">2. वोट करें</string>\n\n    <string name=\"sponsor_kodee_deadline\">वोटिंग 22 मार्च को बंद होगी</string>\n\n    <string name=\"sponsor_kodee_step1\">1. प्लेटफॉर्म पर रजिस्टर करें (Google से जारी रखें)</string>\n    <string name=\"sponsor_kodee_step2\">2. नीचे वोट बटन दबाएँ</string>\n    <string name=\"sponsor_kodee_step3\">3. Usmon Narzullayev खोजें और वोट करें</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">GitHub के माध्यम से एक बार या नियमित समर्थन</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">त्वरित एक-बार समर्थन</string>\n\n    <string name=\"sponsor_other_ways_title\">मदद करने के अन्य तरीके</string>\n\n    <string name=\"sponsor_star_repo\">रिपॉजिटरी को स्टार दें</string>\n    <string name=\"sponsor_star_repo_desc\">दूसरों को GitHub Store खोजने में मदद करता है</string>\n\n    <string name=\"sponsor_report_bugs\">बग रिपोर्ट करें</string>\n    <string name=\"sponsor_report_bugs_desc\">ऐप को बेहतर बनाता है</string>\n\n    <string name=\"sponsor_share\">दोस्तों के साथ साझा करें</string>\n    <string name=\"sponsor_share_desc\">अन्य डेवलपर्स तक बात फैलाएँ</string>\n\n    <string name=\"sponsor_thank_you\">हर तरह का समर्थन — चाहे आर्थिक हो या नहीं — इस प्रोजेक्ट को जीवित रखता है। धन्यवाद!</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">इंस्टॉलेशन</string>\n    <string name=\"installer_type_default\">डिफ़ॉल्ट</string>\n    <string name=\"installer_type_default_description\">मानक सिस्टम इंस्टॉल डायलॉग</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">बिना पुष्टि के साइलेंट इंस्टॉल</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku इंस्टॉल नहीं है</string>\n    <string name=\"shizuku_status_not_running\">Shizuku चल नहीं रहा है</string>\n    <string name=\"shizuku_status_permission_needed\">अनुमति आवश्यक</string>\n    <string name=\"shizuku_status_ready\">तैयार</string>\n    <string name=\"shizuku_grant_permission\">अनुमति दें</string>\n    <string name=\"shizuku_install_hint\">साइलेंट इंस्टॉल सक्रिय करने के लिए Shizuku इंस्टॉल करें</string>\n    <string name=\"shizuku_start_hint\">साइलेंट इंस्टॉल सक्रिय करने के लिए Shizuku शुरू करें</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizuku इंस्टॉल विफल, मानक इंस्टॉलर का उपयोग किया जा रहा है</string>\n\n    <string name=\"auto_update_title\">ऐप्स ऑटो-अपडेट करें</string>\n    <string name=\"auto_update_description\">Shizuku के माध्यम से पृष्ठभूमि में स्वचालित रूप से अपडेट डाउनलोड और इंस्टॉल करें</string>\n\n    <string name=\"section_updates\">अपडेट</string>\n    <string name=\"update_check_interval_title\">अपडेट जाँच अंतराल</string>\n    <string name=\"update_check_interval_description\">पृष्ठभूमि में ऐप अपडेट कितनी बार जाँचें</string>\n    <string name=\"interval_3h\">3घ</string>\n    <string name=\"interval_6h\">6घ</string>\n    <string name=\"interval_12h\">12घ</string>\n    <string name=\"interval_24h\">24घ</string>\n\n    <string name=\"add_by_link\">लिंक से जोड़ें</string>\n    <string name=\"link_app_title\">ऐप को रिपॉजिटरी से लिंक करें</string>\n    <string name=\"pick_installed_app\">GitHub रिपॉजिटरी से लिंक करने के लिए एक इंस्टॉल किया हुआ ऐप चुनें</string>\n    <string name=\"search_apps_hint\">ऐप्स खोजें…</string>\n    <string name=\"enter_repo_url\">GitHub रिपॉजिटरी URL</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">सत्यापन हो रहा है…</string>\n    <string name=\"link_and_track\">लिंक करें और ट्रैक करें</string>\n    <string name=\"checking_release\">नवीनतम रिलीज़ की जाँच हो रही है…</string>\n    <string name=\"downloading_for_verification\">सत्यापन के लिए APK डाउनलोड हो रहा है…</string>\n    <string name=\"verifying_signing_key\">साइनिंग कुंजी सत्यापित हो रही है…</string>\n    <string name=\"package_name_mismatch\">पैकेज नाम मेल नहीं खाता: APK %1$s है, लेकिन चयनित ऐप %2$s है</string>\n    <string name=\"signing_key_mismatch_link\">साइनिंग कुंजी मेल नहीं खाती: इस रिपॉजिटरी का APK किसी अन्य डेवलपर द्वारा हस्ताक्षरित है</string>\n    <string name=\"select_asset_title\">इंस्टॉलर चुनें</string>\n    <string name=\"select_asset_description\">अपने इंस्टॉल किए गए ऐप से मिलान करने के लिए APK चुनें</string>\n    <string name=\"download_failed\">डाउनलोड विफल</string>\n    <string name=\"export_apps\">निर्यात</string>\n    <string name=\"import_apps\">आयात</string>\n    <string name=\"import_apps_title\">ऐप्स आयात करें</string>\n    <string name=\"import_apps_description\">अपने ट्रैक किए गए ऐप्स को पुनर्स्थापित करने के लिए निर्यात किया गया JSON पेस्ट करें</string>\n    <string name=\"import_apps_hint\">निर्यात किया गया JSON यहाँ पेस्ट करें…</string>\n    <string name=\"include_pre_releases_title\">प्री-रिलीज़ शामिल करें</string>\n    <string name=\"include_pre_releases_description\">अपडेट की जाँच करते समय प्री-रिलीज़ संस्करणों को ट्रैक करें। अक्षम होने पर, केवल स्थिर रिलीज़ पर विचार किया जाता है।</string>\n    <string name=\"confirm_uninstall_title\">ऐप अनइंस्टॉल करें?</string>\n    <string name=\"confirm_uninstall_message\">क्या आप वाकई %1$s को अनइंस्टॉल करना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती और ऐप डेटा खो सकता है।</string>\n    <string name=\"invalid_github_url\">अमान्य GitHub URL। प्रारूप का उपयोग करें: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">रिपॉजिटरी नहीं मिली: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub API दर सीमा पार हो गई। बाद में पुनः प्रयास करें।</string>\n    <string name=\"failed_to_link\">लिंक करने में विफल: %1$s</string>\n    <string name=\"failed_to_load_apps\">इंस्टॉल किए गए ऐप्स लोड करने में विफल</string>\n    <string name=\"app_linked_success\">%1$s को %2$s/%3$s से लिंक किया गया</string>\n    <string name=\"export_failed\">निर्यात विफल: %1$s</string>\n    <string name=\"import_failed\">आयात विफल: %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d ऐप्स आयात किए गए</string>\n    <string name=\"imported_skipped\">, %1$d छोड़े गए</string>\n    <string name=\"imported_failed\">, %1$d विफल</string>\n    <string name=\"signing_key_changed_title\">साइनिंग कुंजी बदल गई</string>\n    <string name=\"signing_key_changed_message\">इस ऐप का साइनिंग प्रमाणपत्र पहली बार इंस्टॉल होने के बाद बदल गया है।\\n\\nइसका मतलब हो सकता है कि डेवलपर ने अपनी साइनिंग कुंजी बदल दी, या बाइनरी के साथ छेड़छाड़ की गई हो।\\n\\nअपेक्षित: %1$s\\nप्राप्त: %2$s</string>\n    <string name=\"install_anyway\">फिर भी इंस्टॉल करें</string>\n    <string name=\"verified_build\">सत्यापित बिल्ड</string>\n    <string name=\"checking_attestation\">जाँच हो रही है\\u2026</string>\n    <string name=\"assets_title\">संसाधन</string>\n    <string name=\"no_assets_selected\">कोई संसाधन नहीं</string>\n    <string name=\"no_assets_in_list\">इस रिलीज़ से संबंधित कोई संसाधन नहीं</string>\n    <string name=\"assets_selection_label\">संसाधन विकल्प चुनें</string>\n    <string name=\"multiple_assets_info_dialog_title\">कई संसाधन उपलब्ध</string>\n    <string name=\"multiple_assets_info_dialog_text\">इस रिलीज़ के लिए कई इंस्टॉल करने योग्य फ़ाइलें उपलब्ध हैं। कृपया सूची की समीक्षा करें और अपने डिवाइस के लिए उपयुक्त फ़ाइल चुनें।</string>\n    <string name=\"icon_content_description_info\">जानकारी</string>\n    <string name=\"translation_error_retry\">पुनः प्रयास</string>\n    <string name=\"translated_from\">स्वतः पहचाना गया: %1$s</string>\n    <string name=\"select_language\">भाषा चुनें</string>\n    <string name=\"update_package_mismatch\">पैकेज मेल नहीं खाता: APK %1$s है, लेकिन इंस्टॉल किया गया ऐप %2$s है। अपडेट ब्लॉक किया गया।</string>\n    <string name=\"update_signing_key_mismatch\">साइनिंग कुंजी मेल नहीं खाती: अपडेट किसी अन्य डेवलपर द्वारा साइन किया गया था। अपडेट ब्लॉक किया गया।</string>\n    <string name=\"liquid_glass_option_title\">लिक्विड ग्लास इफ़ेक्ट</string>\n    <string name=\"liquid_glass_option_description\">एक चिकने काँच जैसे रूप से इंटरफ़ेस को बेहतर बनाएं</string>\n\n    <string name=\"hide_seen_title\">देखे गए रिपॉजिटरी छुपाएँ</string>\n    <string name=\"hide_seen_description\">पहले से देखे गए रिपॉजिटरी को डिस्कवरी फ़ीड से छुपाएँ</string>\n    <string name=\"clear_seen_history\">देखने का इतिहास साफ़ करें</string>\n    <string name=\"clear_seen_history_description\">सभी देखे गए रिपॉजिटरी रीसेट करें ताकि वे फ़ीड में फिर से दिखें</string>\n    <string name=\"seen_history_cleared\">देखने का इतिहास साफ़ किया गया</string>\n    <string name=\"seen_badge\">देखा गया</string>\n</resources>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-it/strings-it.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">App Installate</string>\n    <string name=\"navigate_back\">Indietro</string>\n    <string name=\"check_for_updates\">Controlla Aggiornamenti</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">Impossibile lanciare %1$s</string>\n    <string name=\"failed_to_open\">Impossibile aprire %1$s</string>\n    <string name=\"failed_to_update\">Impossibile aggiornare %1$s: %2$s</string>\n    <string name=\"update_failed\">Aggiornamento fallito</string>\n    <string name=\"update_all_failed\">Aggiornamento generale fallito: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">Tutte le app sono state aggiornate con successo</string>\n    <string name=\"no_updates_available\">Nessun aggiornamento disponibile</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">Cerca le tue app</string>\n    <string name=\"no_apps_found\">Nessuna app trovata</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">Aggiorna tutto</string>\n    <string name=\"update\">Aggiorna</string>\n    <string name=\"open\">Apri</string>\n    <string name=\"cancel\">Cancella</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">Controllo…</string>\n    <string name=\"updated_successfully\">Aggiornata con successo</string>\n    <string name=\"error_with_message\">Errore: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">Aggiornando %1$d di %2$d</string>\n    <string name=\"currently_updating\">Attualmente: %1$s</string>\n\n\n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">In attesa di autorizzazione…</string>\n    <string name=\"signed_in\">Autenticazione effettuata!</string>\n    <string name=\"redirecting_message\">Ora puoi usare l'applicazione. Reindirizzamento…</string>\n    <string name=\"try_again\">Riprova</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">Errore: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">Inserisci questo codice su GitHub:</string>\n    <string name=\"copy_code\">Copia il codice</string>\n    <string name=\"open_github\">Apri GitHub</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">Sblocca l'esperienza\\ncompleta</string>\n\n    <string name=\"more_requests\">Più richieste</string>\n    <string name=\"more_requests_description\">\n        Effettua l'accesso per ottenere limiti API più alti e evitare interruzioni.\n    </string>\n\n    <string name=\"sign_in_with_github\">Accedi con GitHub</string>\n\n    <string name=\"error_cancelled\">Annullato</string>\n    <string name=\"error_unknown\">Errore sconosciuto</string>\n\n    <string name=\"language_label\">Lingua:</string>\n\n    <string name=\"discover_repositories\">Scopri Repositories</string>\n    <string name=\"search_repositories_hint\">Cerca repo, descrizione…</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">Filtra per lingua</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">%1$d risultati trovati</string>\n\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">Ordina per</string>\n    <string name=\"close\">Chiudi</string>\n\n    <string name=\"sort_most_stars\">Più Stelle</string>\n    <string name=\"sort_most_forks\">Più Forks</string>\n    <string name=\"sort_best_match\">Miglior corrispondenza</string>\n\n    <string name=\"sort_order_descending\">Decrescente</string>\n    <string name=\"sort_order_ascending\">Crescente</string>\n    <string name=\"sort_label\">Ordina</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">Tutte le lingue</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">Ricerca Fallita</string>\n    <string name=\"no_repositories_found\">Nessuna repository trovata</string>\n\n    <!-- Settings -->\n    <string name=\"profile_title\">Profilo</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">ASPETTO</string>\n    <string name=\"section_about\">INFORMAZIONI</string>\n    <string name=\"section_network\">RETE</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">Colore del Tema</string>\n    <string name=\"amoled_black_theme\">Tema Amoled Nero</string>\n    <string name=\"amoled_black_description\">Nero puro per il tema scuro</string>\n    <string name=\"selected_color\">Colore selezionato: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">Versione</string>\n    <string name=\"help_support\">Aiuto &amp; Supporto</string>\n\n    <!-- Account -->\n    <string name=\"logout\">Esci</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">Uscito con successo, reindirizzamento…</string>\n    <string name=\"cache_cleared\">Cache cancellata con successo</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">Attenzione!</string>\n    <string name=\"logout_confirmation\">Sei sicuro di voler uscire?</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">Dinamico</string>\n    <string name=\"theme_ocean\">Oceano</string>\n    <string name=\"theme_purple\">Viola</string>\n    <string name=\"theme_forest\">Foresta</string>\n    <string name=\"theme_slate\">Ardesia</string>\n    <string name=\"theme_amber\">Ambra</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">Apri repository</string>\n    <string name=\"open_in_browser\">Apri nel browser</string>\n    <string name=\"cancel_download\">Annulla download</string>\n    <string name=\"show_install_options\">Mostra opzioni di installazione</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">Errore nel caricamento dei dettagli</string>\n    <string name=\"retry\">Riprova</string>\n    <string name=\"no_description\">Nessuna descrizione fornita.</string>\n    <string name=\"no_release_notes\">Nessuna nota di aggiornamento.</string>\n    <string name=\"report_issue\">Segnala problema</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">Informazioni su quest'app</string>\n    <string name=\"install_logs\">Installa logs</string>\n    <string name=\"author\">Autore</string>\n    <string name=\"whats_new\">Cosa c'è di nuovo</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">Installate</string>\n    <string name=\"update_available\">Aggiornamento disponibile</string>\n    <string name=\"not_available\">Non disponibile</string>\n    <string name=\"install_latest\">Installa l'ultima versione</string>\n    <string name=\"reinstall\">Reinstalla</string>\n    <string name=\"update_app\">Aggiorna app</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">Scaricamento</string>\n    <string name=\"updating\">Aggiornamento</string>\n    <string name=\"verifying\">Verifica</string>\n    <string name=\"installing\">Installazione</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Apri in Obtainium</string>\n    <string name=\"obtainium_description\">Gestisci aggiornamenti automaticamente</string>\n    <string name=\"inspect_with_appmanager\">Ispeziona con AppManager</string>\n    <string name=\"appmanager_description\">Controlla permessi, trackers &amp; sicurezza</string>\n\n    <!-- Author -->\n    <string name=\"profile\">Profilo</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">Forks</string>\n    <string name=\"stars\">Stelle</string>\n    <string name=\"issues\">Problemi</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">per %1$s</string>\n    <string name=\"installed_version\">• Installate: %1$s</string>\n    <string name=\"architecture_compatible\">Architettura compatibile</string>\n    <string name=\"update_to_version\">Aggiorna a %1$s</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">Impossibile caricare i dettagli</string>\n    <string name=\"installer_saved_downloads\">L'installer è stato salvato nella cartella di download</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">Download iniziato</string>\n    <string name=\"log_downloaded\">Scaricato</string>\n    <string name=\"log_update_started\">Aggiornamento iniziato</string>\n    <string name=\"log_installed\">Installato</string>\n    <string name=\"log_updated\">Aggiornato</string>\n    <string name=\"log_cancelled\">Cancellato</string>\n    <string name=\"log_install_started\">Installazione iniziata</string>\n    <string name=\"log_error\">Errore</string>\n    <string name=\"log_error_with_message\">Errore: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">Preparazione dell'AppManager</string>\n    <string name=\"log_opened_appmanager\">Aperto nell'AppManager</string>\n    <string name=\"log_permission_blocked\">Permesso di installazione bloccato dalla policy del dispositivo</string>\n    <string name=\"log_opened_external_installer\">Aperto nell\\'installatore esterno</string>\n    <string name=\"install_permission_unavailable\">Permesso di installazione non disponibile</string>\n    <string name=\"install_permission_blocked_message\">L\\'APK è stato scaricato con successo ma questo dispositivo non consente l\\'installazione diretta. Vuoi aprirlo con un installatore esterno?</string>\n    <string name=\"open_with_external_installer\">Apri con installatore esterno</string>\n    <string name=\"external_installer_description\">Usa un\\'app di terze parti per installare l\\'APK</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">Errore: %1$s</string>\n    <string name=\"error_asset_not_supported\">Tipo di archivio .%1$s non supportato</string>\n    <string name=\"error_file_not_found\">File scaricato non trovato</string>\n\n    <string name=\"home_category_trending\">In tendenza</string>\n    <string name=\"home_category_hot_release\">Rilascio a caldo</string>\n    <string name=\"home_category_most_popular\">I più popolari</string>\n\n    <string name=\"home_finding_repositories\">Ricerca di repositories...</string>\n    <string name=\"home_loading_more\">Caricamento in corso...</string>\n    <string name=\"home_no_more_repositories\">Non ci sono più repositories</string>\n    <string name=\"home_retry\">Riprova</string>\n    <string name=\"home_failed_to_load_repositories\">Impossibile caricare repositories</string>\n    <string name=\"home_view_details\">Vedi Dettagli</string>\n\n    <string name=\"updated_just_now\">appena aggiornata</string>\n    <string name=\"updated_hours_ago\">aggiornata %1$d ora/e fa</string>\n    <string name=\"updated_yesterday\">aggiornata ieri</string>\n    <string name=\"updated_days_ago\">aggiornata %1$d giorno/i fa</string>\n    <string name=\"updated_on_date\">aggiornata il %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">Superato limite di richieste</string>\n    <string name=\"rate_limit_used_all\">Hai usato tutte le %1$d richieste API.</string>\n    <string name=\"rate_limit_used_all_free\">Hai usato tutte le %1$d richieste API gratuite.</string>\n    <string name=\"rate_limit_resets_in_minutes\">Si resetta tra %1$d minuti</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Effettua l'accesso per ottenere 5,000 richieste all'ora invece di 60!</string>\n    <string name=\"rate_limit_sign_in\">Accesso</string>\n    <string name=\"rate_limit_ok\">OK</string>\n    <string name=\"rate_limit_close\">Chiudi</string>\n\n    <string name=\"system_font\">Font del sistema</string>\n    <string name=\"system_font_description\">Usa il font di sistema per una migliore leggibilità</string>\n\n    <string name=\"added_to_favourites\">Repository aggiunto ai preferiti</string>\n    <string name=\"removed_from_favourites\">Repository rimosso dai preferiti</string>\n    <string name=\"add_to_favourites\">Aggiungi ai preferiti</string>\n    <string name=\"remove_from_favourites\">Rimuovi dai preferiti</string>\n    <string name=\"favourites\">Preferiti</string>\n\n    <string name=\"added_just_now\">aggiunto poco fa</string>\n    <string name=\"added_hours_ago\">aggiunto %1$d ora(e) fa</string>\n    <string name=\"added_yesterday\">aggiunto ieri</string>\n    <string name=\"added_days_ago\">aggiunto %1$d giorno(i) fa</string>\n    <string name=\"added_on_date\">aggiunto il %1$s</string>\n\n    <string name=\"starred_repositories\">Repository preferiti</string>\n    <string name=\"repository_starred\">Il repository è nei preferiti</string>\n    <string name=\"repository_not_starred\">Il repository non è nei preferiti</string>\n    <string name=\"star_from_github\">Puoi aggiungere il repository ai preferiti su GitHub</string>\n    <string name=\"unstar_from_github\">Puoi rimuovere il repository dai preferiti su GitHub</string>\n\n    <string name=\"sign_in_required\">Accesso richiesto</string>\n    <string name=\"no_starred_repos\">Nessun repository preferito</string>\n    <string name=\"sign_in_with_github_for_stars\">Accedi con GitHub per vedere i repository preferiti</string>\n    <string name=\"star_repos_hint\">Aggiungi repository con release installabili su GitHub per vederli qui</string>\n    <string name=\"last_synced\">Ultima sincronizzazione</string>\n    <string name=\"just_now\">Poco fa</string>\n    <string name=\"minutes_ago\">%1$d min fa</string>\n    <string name=\"hours_ago\">%1$d h fa</string>\n    <string name=\"days_ago\">%1$d g fa</string>\n    <string name=\"dismiss\">Chiudi</string>\n    <string name=\"sync_starred_failed\">Impossibile sincronizzare i repository preferiti</string>\n\n    <string name=\"developer_profile_title\">Profilo dello sviluppatore</string>\n    <string name=\"open_developer_profile\">Apri il profilo dello sviluppatore</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">Impossibile caricare i repository</string>\n    <string name=\"failed_to_load_profile\">Impossibile caricare il profilo</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Repository</string>\n    <string name=\"followers\">Follower</string>\n    <string name=\"following\">Seguiti</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Cerca repository…</string>\n    <string name=\"clear_search\">Cancella ricerca</string>\n    <string name=\"filter_all\">Tutti</string>\n    <string name=\"filter_with_releases\">Con release</string>\n    <string name=\"filter_installed\">Installati</string>\n    <string name=\"filter_favorites\">Preferiti</string>\n    <string name=\"sort\">Ordina</string>\n    <string name=\"sort_recently_updated\">Aggiornati di recente</string>\n    <string name=\"sort_name\">Nome</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">repository</string>\n    <string name=\"repositories_plural\">repository</string>\n    <string name=\"showing_x_of_y_repositories\">Visualizzazione di %1$d su %2$d repository</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">Nessun repository con release installabili</string>\n    <string name=\"no_installed_repos\">Nessun repository installato</string>\n    <string name=\"no_favorite_repos\">Nessun repository preferito</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">Aggiornato %1$s fa</string>\n    <string name=\"has_release\">Ha una release</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d a fa</string>\n    <string name=\"time_months_ago\">%1$d m fa</string>\n    <string name=\"time_days_ago\">%1$d g fa</string>\n    <string name=\"time_hours_ago\">%1$d h fa</string>\n    <string name=\"time_minutes_ago\">%1$d min fa</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">Pubblicato ora</string>\n    <string name=\"released_hours_ago\">Pubblicato %1$d ora/e fa</string>\n    <string name=\"released_yesterday\">Pubblicato ieri</string>\n    <string name=\"released_days_ago\">Pubblicato %1$d giorno/i fa</string>\n    <string name=\"released_on_date\">Pubblicato il %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">Home</string>\n    <string name=\"bottom_nav_search_title\">Cerca</string>\n    <string name=\"bottom_nav_apps_title\">App</string>\n    <string name=\"bottom_nav_profile_title\">Profilo</string>\n\n    <string name=\"forked_repository\">Fork</string>\n\n    <string name=\"category_stable\">Stabile</string>\n    <string name=\"category_pre_release\">Pre-release</string>\n    <string name=\"category_all\">Tutte</string>\n    <string name=\"select_version\">Seleziona versione</string>\n    <string name=\"pre_release_badge\">Pre-release</string>\n    <string name=\"no_version_selected\">Nessuna versione selezionata</string>\n    <string name=\"versions_title\">Versioni</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">Installazione in sospeso</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Disinstalla</string>\n    <string name=\"open_app\">Apri</string>\n    <string name=\"downgrade_requires_uninstall\">Il downgrade richiede la disinstallazione</string>\n    <string name=\"downgrade_warning_message\">L\\'installazione della versione %1$s richiede la disinstallazione della versione corrente (%2$s). I dati dell\\'app verranno persi.</string>\n    <string name=\"uninstall_first\">Disinstalla prima</string>\n    <string name=\"install_version\">Installa %1$s</string>\n    <string name=\"failed_to_open_app\">Impossibile aprire %1$s</string>\n    <string name=\"failed_to_uninstall\">Impossibile disinstallare %1$s</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">Ultima</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_light\">Chiaro</string>\n    <string name=\"theme_dark\">Scuro</string>\n    <string name=\"theme_system\">Sistema</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Ultimo controllo: %1$s</string>\n    <string name=\"last_checked_never\">Mai controllato</string>\n    <string name=\"last_checked_just_now\">proprio ora</string>\n    <string name=\"last_checked_minutes_ago\">%1$d min fa</string>\n    <string name=\"last_checked_hours_ago\">%1$d h fa</string>\n    <string name=\"checking_for_updates\">Controllo aggiornamenti…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Tipo di proxy</string>\n    <string name=\"proxy_none\">Nessuno</string>\n    <string name=\"proxy_system\">Sistema</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Host</string>\n    <string name=\"proxy_port\">Porta</string>\n    <string name=\"proxy_username\">Nome utente (facoltativo)</string>\n    <string name=\"proxy_password\">Password (facoltativo)</string>\n    <string name=\"proxy_save\">Salva Proxy</string>\n    <string name=\"proxy_saved\">Impostazioni proxy salvate</string>\n    <string name=\"proxy_system_description\">Usa le impostazioni proxy del dispositivo</string>\n    <string name=\"proxy_port_error\">La porta deve essere 1–65535</string>\n    <string name=\"proxy_none_description\">Connessione diretta, nessun proxy</string>\n    <string name=\"failed_to_save_proxy_settings\">Impossibile salvare le impostazioni del proxy</string>\n    <string name=\"proxy_host_required\">L'host del proxy è obbligatorio</string>\n    <string name=\"invalid_proxy_port\">Porta proxy non valida</string>\n    <string name=\"proxy_show_password\">Mostra password</string>\n    <string name=\"proxy_hide_password\">Nascondi password</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Traccia questa app</string>\n    <string name=\"app_tracked_successfully\">App aggiunta alla lista di monitoraggio</string>\n    <string name=\"failed_to_track_app\">Impossibile tracciare l\\'app: %1$s</string>\n    <string name=\"already_tracked\">L\\'app è già monitorata</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">Accedi a GitHub</string>\n    <string name=\"profile_sign_in_description\">Sblocca l\\'esperienza completa. Gestisci le tue app, sincronizza le preferenze e naviga più velocemente.</string>\n    <string name=\"profile_repos\">Repo</string>\n    <string name=\"profile_login\">Accedi</string>\n    <string name=\"profile_stars_description\">I tuoi repository preferiti su GitHub</string>\n    <string name=\"profile_favourites_description\">I tuoi repository preferiti salvati localmente</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Sessione scaduta</string>\n    <string name=\"session_expired_message\">La tua sessione GitHub è scaduta o il token è stato revocato. Accedi di nuovo per continuare a utilizzare le funzionalità autenticate.</string>\n    <string name=\"session_expired_hint\">Puoi continuare a navigare come ospite con richieste API limitate.</string>\n    <string name=\"sign_in_again\">Accedi di nuovo</string>\n    <string name=\"continue_as_guest\">Continua come ospite</string>\n    <string name=\"logout_revocation_note\">Questo cancellerà la sessione locale e i dati nella cache. Per revocare completamente l\\'accesso, visita GitHub Settings > Applications.</string>\n    <string name=\"auth_code_expires_in\">Il codice scade tra %1$s</string>\n    <string name=\"auth_error_code_expired\">Il codice del dispositivo è scaduto.</string>\n    <string name=\"auth_hint_try_again\">Riprova ad accedere per ottenere un nuovo codice.</string>\n    <string name=\"auth_hint_check_connection\">Controlla la tua connessione internet e riprova.</string>\n    <string name=\"auth_hint_denied\">Hai rifiutato la richiesta di autorizzazione. Riprova se è stato involontario.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Leggi di più</string>\n    <string name=\"show_less\">Mostra meno</string>\n\n    <string name=\"share_repository\">Condividi repository</string>\n    <string name=\"failed_to_share_link\">Impossibile condividere il link</string>\n    <string name=\"link_copied_to_clipboard\">Link copiato negli appunti</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Traduci</string>\n    <string name=\"translating\">Traduzione…</string>\n    <string name=\"show_original\">Mostra originale</string>\n    <string name=\"translated_to\">Tradotto in %1$s</string>\n    <string name=\"translate_to\">Traduci in…</string>\n    <string name=\"search_language\">Cerca lingua</string>\n    <string name=\"change_language\">Cambia lingua</string>\n    <string name=\"translation_failed\">Traduzione fallita. Riprova.</string>\n\n    <string name=\"open_github_link\">Apri link GitHub</string>\n    <string name=\"clipboard_link_detected\">Link GitHub rilevato negli appunti</string>\n    <string name=\"auto_detect_clipboard_links\">Rileva link dagli appunti</string>\n    <string name=\"auto_detect_clipboard_description\">Rileva automaticamente i link GitHub dagli appunti all\\'apertura della ricerca</string>\n    <string name=\"detected_links\">Link rilevati</string>\n    <string name=\"open_in_app\">Apri nell\\'app</string>\n    <string name=\"no_github_link_in_clipboard\">Nessun link GitHub trovato negli appunti</string>\n\n    <string name=\"storage\">Archiviazione</string>\n    <string name=\"clear_cache\">Pulisci cache</string>\n    <string name=\"current_size\">Dimensione attuale:</string>\n    <string name=\"clear\">Pulisci</string>\n\n    <string name=\"sponsor_title\">Supporta GitHub Store</string>\n    <string name=\"sponsor_button\">Supporta il progetto</string>\n    <string name=\"sponsor_hero_title\">Costruito con amore,\\nmantenuto con il caffè</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store ha raggiunto oltre 130.000 download e 7.700 stelle su GitHub — 100% gratuito, senza pubblicità né tracciamento.</string>\n\n    <string name=\"sponsor_personal_note\">Ho costruito e mantengo questo progetto completamente da solo mentre finisco il liceo. Il tuo supporto — anche piccolo — aiuta a mantenere l'app senza bug e a finanziare l'infrastruttura.</string>\n\n    <string name=\"sponsor_kodee_title\">Vota GitHub Store!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store è nominato ai Golden Kodee Awards al KotlinConf 2026.</string>\n\n    <string name=\"sponsor_kodee_register\">1. Registrati</string>\n    <string name=\"sponsor_kodee_vote\">2. Vota</string>\n    <string name=\"sponsor_kodee_deadline\">Le votazioni chiudono il 22 marzo</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Registrati sulla piattaforma (Continua con Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. Tocca Vota qui sotto</string>\n    <string name=\"sponsor_kodee_step3\">3. Trova Usmon Narzullayev e premi Vota</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">Supporto ricorrente o singolo via GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Supporto rapido una tantum</string>\n\n    <string name=\"sponsor_other_ways_title\">ALTRI MODI PER AIUTARE</string>\n\n    <string name=\"sponsor_star_repo\">Metti una stella al repository</string>\n    <string name=\"sponsor_star_repo_desc\">Aiuta altri a scoprire GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">Segnala bug</string>\n    <string name=\"sponsor_report_bugs_desc\">Rende l'app migliore per tutti</string>\n\n    <string name=\"sponsor_share\">Condividi con amici</string>\n    <string name=\"sponsor_share_desc\">Diffondi la parola tra gli sviluppatori</string>\n\n    <string name=\"sponsor_thank_you\">Ogni supporto — finanziario o meno — mantiene vivo questo progetto. Grazie!</string>\n\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Installazione</string>\n    <string name=\"installer_type_default\">Predefinito</string>\n    <string name=\"installer_type_default_description\">Finestra di installazione standard del sistema</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Installazione silenziosa senza conferme</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku non è installato</string>\n    <string name=\"shizuku_status_not_running\">Shizuku non è in esecuzione</string>\n    <string name=\"shizuku_status_permission_needed\">Autorizzazione necessaria</string>\n    <string name=\"shizuku_status_ready\">Pronto</string>\n    <string name=\"shizuku_grant_permission\">Concedi autorizzazione</string>\n    <string name=\"shizuku_install_hint\">Installa Shizuku per abilitare l\\'installazione silenziosa</string>\n    <string name=\"shizuku_start_hint\">Avvia Shizuku per abilitare l\\'installazione silenziosa</string>\n    <string name=\"shizuku_install_failed_fallback\">Installazione tramite Shizuku fallita, utilizzo dell\\'installatore standard</string>\n\n    <string name=\"auto_update_title\">Aggiornamento automatico</string>\n    <string name=\"auto_update_description\">Scarica e installa automaticamente gli aggiornamenti in background tramite Shizuku</string>\n\n    <string name=\"section_updates\">Aggiornamenti</string>\n    <string name=\"update_check_interval_title\">Intervallo di controllo</string>\n    <string name=\"update_check_interval_description\">Ogni quanto verificare gli aggiornamenti in background</string>\n    <string name=\"interval_3h\">3h</string>\n    <string name=\"interval_6h\">6h</string>\n    <string name=\"interval_12h\">12h</string>\n    <string name=\"interval_24h\">24h</string>\n\n    <string name=\"add_by_link\">Aggiungi tramite link</string>\n    <string name=\"link_app_title\">Collega app al repository</string>\n    <string name=\"pick_installed_app\">Scegli un\\'app installata da collegare a un repository GitHub</string>\n    <string name=\"search_apps_hint\">Cerca app…</string>\n    <string name=\"enter_repo_url\">URL del repository GitHub</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Validazione…</string>\n    <string name=\"link_and_track\">Collega e monitora</string>\n    <string name=\"checking_release\">Controllo dell\\'ultima versione…</string>\n    <string name=\"downloading_for_verification\">Download APK per la verifica…</string>\n    <string name=\"verifying_signing_key\">Verifica della chiave di firma…</string>\n    <string name=\"package_name_mismatch\">Nome pacchetto diverso: l\\'APK è %1$s, ma l\\'app selezionata è %2$s</string>\n    <string name=\"signing_key_mismatch_link\">Chiave di firma diversa: l\\'APK di questo repository è stato firmato da uno sviluppatore diverso</string>\n    <string name=\"select_asset_title\">Seleziona installatore</string>\n    <string name=\"select_asset_description\">Scegli l\\'APK da verificare con la tua app installata</string>\n    <string name=\"download_failed\">Download fallito</string>\n    <string name=\"export_apps\">Esporta</string>\n    <string name=\"import_apps\">Importa</string>\n    <string name=\"import_apps_title\">Importa app</string>\n    <string name=\"import_apps_description\">Incolla il JSON esportato per ripristinare le app monitorate</string>\n    <string name=\"import_apps_hint\">Incolla il JSON esportato qui…</string>\n    <string name=\"include_pre_releases_title\">Includi pre-release</string>\n    <string name=\"include_pre_releases_description\">Monitora le versioni pre-release durante il controllo aggiornamenti. Se disabilitato, vengono considerate solo le versioni stabili.</string>\n    <string name=\"confirm_uninstall_title\">Disinstallare l\\'app?</string>\n    <string name=\"confirm_uninstall_message\">Sei sicuro di voler disinstallare %1$s? Questa azione non può essere annullata e i dati dell\\'app potrebbero andare persi.</string>\n    <string name=\"invalid_github_url\">URL GitHub non valido. Usa il formato: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Repository non trovato: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">Limite API GitHub superato. Riprova più tardi.</string>\n    <string name=\"failed_to_link\">Collegamento fallito: %1$s</string>\n    <string name=\"failed_to_load_apps\">Impossibile caricare le app installate</string>\n    <string name=\"app_linked_success\">%1$s collegata a %2$s/%3$s</string>\n    <string name=\"export_failed\">Esportazione fallita: %1$s</string>\n    <string name=\"import_failed\">Importazione fallita: %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d app importate</string>\n    <string name=\"imported_skipped\">, %1$d saltate</string>\n    <string name=\"imported_failed\">, %1$d fallite</string>\n    <string name=\"signing_key_changed_title\">Chiave di firma cambiata</string>\n    <string name=\"signing_key_changed_message\">Il certificato di firma di questa app è cambiato dalla prima installazione.\\n\\nQuesto potrebbe significare che lo sviluppatore ha cambiato la chiave di firma, o il binario potrebbe essere stato alterato.\\n\\nPrevisto: %1$s\\nRicevuto: %2$s</string>\n    <string name=\"install_anyway\">Installa comunque</string>\n    <string name=\"verified_build\">Build verificata</string>\n    <string name=\"checking_attestation\">Controllo\\u2026</string>\n    <string name=\"assets_title\">Risorse</string>\n    <string name=\"no_assets_selected\">Nessuna risorsa</string>\n    <string name=\"no_assets_in_list\">Nessuna risorsa associata a questa versione</string>\n    <string name=\"assets_selection_label\">Seleziona opzione risorsa</string>\n    <string name=\"multiple_assets_info_dialog_title\">Risorse multiple disponibili</string>\n    <string name=\"multiple_assets_info_dialog_text\">Ci sono più file installabili disponibili per questa versione. Controlla la lista e seleziona quello adatto al tuo dispositivo.</string>\n    <string name=\"icon_content_description_info\">Informazioni</string>\n    <string name=\"translation_error_retry\">Riprova</string>\n    <string name=\"translated_from\">Rilevato automaticamente: %1$s</string>\n    <string name=\"select_language\">Seleziona lingua</string>\n    <string name=\"update_package_mismatch\">Pacchetto non corrispondente: l\\'APK è %1$s, ma l\\'app installata è %2$s. Aggiornamento bloccato.</string>\n    <string name=\"update_signing_key_mismatch\">Chiave di firma non corrispondente: l\\'aggiornamento è stato firmato da uno sviluppatore diverso. Aggiornamento bloccato.</string>\n    <string name=\"liquid_glass_option_title\">Effetto vetro liquido</string>\n    <string name=\"liquid_glass_option_description\">Migliora l\\'interfaccia con un aspetto liscio simile al vetro</string>\n\n    <string name=\"hide_seen_title\">Nascondi repository visualizzati</string>\n    <string name=\"hide_seen_description\">Nascondi i repository già visualizzati dai feed di scoperta</string>\n    <string name=\"clear_seen_history\">Cancella cronologia visualizzazioni</string>\n    <string name=\"clear_seen_history_description\">Reimposta tutti i repository visualizzati in modo che ricompaiano nei feed</string>\n    <string name=\"seen_history_cleared\">Cronologia visualizzazioni cancellata</string>\n    <string name=\"seen_badge\">Visualizzato</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml",
    "content": "<resources>\n    <string name=\"app_name\">GitHub Store</string>\n\n    <string name=\"installed_apps\">インストール済みアプリ</string>\n    <string name=\"navigate_back\">戻る</string>\n    <string name=\"check_for_updates\">更新を確認</string>\n\n    <string name=\"cannot_launch\">%1$s を起動できません</string>\n    <string name=\"failed_to_open\">%1$s を開けませんでした</string>\n    <string name=\"failed_to_update\">%1$s の更新に失敗しました：%2$s</string>\n    <string name=\"update_failed\">更新に失敗しました</string>\n    <string name=\"update_all_failed\">すべての更新に失敗しました：%1$s</string>\n    <string name=\"all_apps_updated_successfully\">すべてのアプリが更新されました</string>\n    <string name=\"no_updates_available\">更新はありません</string>\n\n    <string name=\"search_your_apps\">アプリを検索</string>\n    <string name=\"no_apps_found\">アプリが見つかりません</string>\n\n    <string name=\"update_all\">すべて更新</string>\n    <string name=\"update\">更新</string>\n    <string name=\"open\">開く</string>\n    <string name=\"cancel\">キャンセル</string>\n\n    <string name=\"checking\">確認中…</string>\n    <string name=\"updated_successfully\">更新完了</string>\n    <string name=\"error_with_message\">エラー：%1$s</string>\n\n    <string name=\"updating_x_of_y\">%2$d 件中 %1$d 件を更新中</string>\n    <string name=\"currently_updating\">現在：%1$s</string>\n\n    <string name=\"waiting_for_authorization\">認証を待機中…</string>\n    <string name=\"signed_in\">ログイン完了！</string>\n    <string name=\"redirecting_message\">アプリを使用できます。リダイレクト中…</string>\n    <string name=\"try_again\">再試行</string>\n\n    <string name=\"auth_error_with_message\">エラー：%1$s</string>\n\n    <string name=\"enter_code_on_github\">GitHubでこのコードを入力してください：</string>\n    <string name=\"copy_code\">コードをコピー</string>\n    <string name=\"open_github\">GitHubを開く</string>\n\n    <string name=\"unlock_full_experience\">すべての機能を\\n解放</string>\n\n    <string name=\"more_requests\">リクエスト数を増加</string>\n    <string name=\"more_requests_description\">\n        ログインして API の制限を引き上げ、中断を防ぎましょう。\n    </string>\n\n    <string name=\"sign_in_with_github\">GitHubでサインイン</string>\n\n    <string name=\"error_cancelled\">キャンセルされました</string>\n    <string name=\"error_unknown\">不明なエラー</string>\n\n    <string name=\"language_label\">言語：</string>\n\n    <string name=\"discover_repositories\">リポジトリを探索</string>\n    <string name=\"search_repositories_hint\">リポジトリや説明を検索…</string>\n\n    <string name=\"filter_by_language\">言語でフィルター</string>\n\n    <string name=\"results_found\">%1$d 件の結果</string>\n\n    <string name=\"retry\">再試行</string>\n\n    <string name=\"sort_by\">並び替え</string>\n    <string name=\"close\">閉じる</string>\n\n    <string name=\"sort_most_stars\">スター数順</string>\n    <string name=\"sort_most_forks\">フォーク数順</string>\n    <string name=\"sort_best_match\">最適な一致</string>\n\n    <string name=\"sort_order_descending\">降順</string>\n    <string name=\"sort_order_ascending\">昇順</string>\n    <string name=\"sort_label\">並び替え</string>\n\n    <string name=\"language_all\">すべての言語</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">検索に失敗しました</string>\n    <string name=\"no_repositories_found\">リポジトリが見つかりませんでした</string>\n\n    <string name=\"profile_title\">プロフィール</string>\n\n    <string name=\"section_appearance\">外観</string>\n    <string name=\"section_about\">情報</string>\n    <string name=\"section_network\">ネットワーク</string>\n\n    <string name=\"theme_color\">テーマカラー</string>\n    <string name=\"amoled_black_theme\">AMOLED ブラックテーマ</string>\n    <string name=\"amoled_black_description\">ダークモード用の純黒背景</string>\n    <string name=\"selected_color\">選択された色：%1$s</string>\n\n    <string name=\"version\">バージョン</string>\n    <string name=\"help_support\">ヘルプとサポート</string>\n\n    <string name=\"logout\">ログアウト</string>\n\n    <string name=\"logout_success\">ログアウトしました。リダイレクト中…</string>\n    <string name=\"cache_cleared\">キャッシュを正常にクリアしました</string>\n\n    <string name=\"warning\">警告！</string>\n    <string name=\"logout_confirmation\">ログアウトしてもよろしいですか？</string>\n\n    <string name=\"theme_dynamic\">ダイナミック</string>\n    <string name=\"theme_ocean\">オーシャン</string>\n    <string name=\"theme_purple\">パープル</string>\n    <string name=\"theme_forest\">フォレスト</string>\n    <string name=\"theme_slate\">スレート</string>\n    <string name=\"theme_amber\">アンバー</string>\n\n    <string name=\"error_loading_details\">詳細の読み込みに失敗しました</string>\n    <string name=\"about_this_app\">このアプリについて</string>\n    <string name=\"install_logs\">インストールログ</string>\n    <string name=\"author\">作者</string>\n    <string name=\"whats_new\">新機能</string>\n    <string name=\"report_issue\">問題を報告</string>\n\n    <string name=\"installed\">インストール済み</string>\n    <string name=\"update_available\">更新あり</string>\n    <string name=\"install_latest\">最新版をインストール</string>\n    <string name=\"reinstall\">再インストール</string>\n\n    <string name=\"downloading\">ダウンロード中</string>\n    <string name=\"updating\">更新中</string>\n    <string name=\"verifying\">検証中</string>\n    <string name=\"installing\">インストール中</string>\n\n    <string name=\"profile\">プロフィール</string>\n    <string name=\"forks\">フォーク</string>\n    <string name=\"stars\">スター</string>\n    <string name=\"issues\">課題</string>\n\n    <string name=\"by_author\">%1$s 作</string>\n    <string name=\"installed_version\">• インストール済み: %1$s</string>\n    <string name=\"architecture_compatible\">アーキテクチャ互換</string>\n    <string name=\"update_to_version\">%1$s に更新</string>\n\n    <string name=\"error_load_details\">詳細を読み込めませんでした</string>\n    <string name=\"installer_saved_downloads\">インストーラーはダウンロードに保存されました</string>\n\n    <string name=\"log_download_started\">ダウンロード開始</string>\n    <string name=\"log_downloaded\">ダウンロード完了</string>\n    <string name=\"log_update_started\">更新開始</string>\n    <string name=\"log_installed\">インストール済み</string>\n    <string name=\"log_updated\">更新済み</string>\n    <string name=\"log_cancelled\">キャンセルされました</string>\n    <string name=\"log_install_started\">インストールを開始しました</string>\n    <string name=\"log_error\">エラー</string>\n    <string name=\"log_error_with_message\">エラー: %1$s</string>\n\n\n    <string name=\"log_prepare_appmanager\">AppManager 用に準備中</string>\n    <string name=\"log_opened_appmanager\">AppManager で開きました</string>\n    <string name=\"log_permission_blocked\">デバイスポリシーによりインストール権限がブロックされました</string>\n    <string name=\"log_opened_external_installer\">外部インストーラーで開きました</string>\n    <string name=\"install_permission_unavailable\">インストール権限が利用できません</string>\n    <string name=\"install_permission_blocked_message\">APKは正常にダウンロードされましたが、このデバイスでは直接インストールが許可されていません。外部インストーラーで開きますか？</string>\n    <string name=\"open_with_external_installer\">外部インストーラーで開く</string>\n    <string name=\"external_installer_description\">サードパーティアプリを使用してAPKをインストール</string>\n\n    <string name=\"error_generic\">エラー: %1$s</string>\n    <string name=\"error_asset_not_supported\">ファイル形式 .%1$s は未対応です</string>\n    <string name=\"error_file_not_found\">ダウンロードしたファイルが見つかりません</string>\n\n    <string name=\"home_category_trending\">トレンド</string>\n    <string name=\"home_category_hot_release\">ホットリリース</string>\n    <string name=\"home_category_most_popular\">最も人気のある</string>\n\n    <string name=\"home_finding_repositories\">リポジトリを検索中…</string>\n    <string name=\"home_loading_more\">読み込み中…</string>\n    <string name=\"home_no_more_repositories\">これ以上ありません</string>\n    <string name=\"home_retry\">再試行</string>\n    <string name=\"home_failed_to_load_repositories\">リポジトリの読み込みに失敗しました</string>\n    <string name=\"home_view_details\">詳細を見る</string>\n\n    <string name=\"updated_just_now\">たった今更新</string>\n    <string name=\"updated_hours_ago\">%1$d時間前に更新</string>\n    <string name=\"updated_yesterday\">昨日更新</string>\n    <string name=\"updated_days_ago\">%1$d日前に更新</string>\n    <string name=\"updated_on_date\">%1$s に更新</string>\n\n    <string name=\"rate_limit_exceeded\">レート制限を超えました</string>\n    <string name=\"rate_limit_used_all\">%1$d 件のAPIリクエストをすべて使用しました。</string>\n    <string name=\"rate_limit_used_all_free\">%1$d 件の無料APIリクエストをすべて使用しました。</string>\n    <string name=\"rate_limit_resets_in_minutes\">%1$d分後にリセット</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 サインインすると、1時間あたり5,000件のリクエストが可能です。</string>\n    <string name=\"rate_limit_sign_in\">サインイン</string>\n    <string name=\"rate_limit_ok\">OK</string>\n    <string name=\"rate_limit_close\">閉じる</string>\n\n    <string name=\"system_font\">システムフォント</string>\n    <string name=\"system_font_description\">デバイスのフォントを使用して読みやすさを向上</string>\n\n    <string name=\"theme_light\">ライト</string>\n    <string name=\"theme_dark\">ダーク</string>\n    <string name=\"theme_system\">システム</string>\n\n    <string name=\"added_to_favourites\">リポジトリをお気に入りに追加しました</string>\n    <string name=\"removed_from_favourites\">リポジトリをお気に入りから削除しました</string>\n    <string name=\"add_to_favourites\">お気に入りに追加</string>\n    <string name=\"remove_from_favourites\">お気に入りから削除</string>\n    <string name=\"favourites\">お気に入り</string>\n\n    <string name=\"added_just_now\">たった今追加されました</string>\n    <string name=\"added_hours_ago\">%1$d時間前に追加</string>\n    <string name=\"added_yesterday\">昨日追加</string>\n    <string name=\"added_days_ago\">%1$d日前に追加</string>\n    <string name=\"added_on_date\">%1$s に追加</string>\n\n    <string name=\"starred_repositories\">スター付きリポジトリ</string>\n    <string name=\"repository_starred\">リポジトリはスター済みです</string>\n    <string name=\"repository_not_starred\">リポジトリはスターされていません</string>\n    <string name=\"star_from_github\">GitHub でリポジトリにスターを付けられます</string>\n    <string name=\"unstar_from_github\">GitHub でスターを解除できます</string>\n\n    <string name=\"sign_in_required\">ログインが必要です</string>\n    <string name=\"no_starred_repos\">スター付きリポジトリはありません</string>\n    <string name=\"sign_in_with_github_for_stars\">GitHubでログインしてスター付きリポジトリを表示します</string>\n    <string name=\"star_repos_hint\">インストール可能なリリースのあるリポジトリにスターを付けてください</string>\n    <string name=\"last_synced\">最終同期</string>\n    <string name=\"just_now\">たった今</string>\n    <string name=\"minutes_ago\">%1$d分前</string>\n    <string name=\"hours_ago\">%1$d時間前</string>\n    <string name=\"days_ago\">%1$d日前</string>\n    <string name=\"dismiss\">閉じる</string>\n    <string name=\"sync_starred_failed\">スター付きリポジトリの同期に失敗しました</string>\n\n    <string name=\"developer_profile_title\">開発者プロフィール</string>\n    <string name=\"open_developer_profile\">開発者プロフィールを開く</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">リポジトリの読み込みに失敗しました</string>\n    <string name=\"failed_to_load_profile\">プロフィールの読み込みに失敗しました</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">リポジトリ</string>\n    <string name=\"followers\">フォロワー</string>\n    <string name=\"following\">フォロー中</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">リポジトリを検索…</string>\n    <string name=\"clear_search\">検索をクリア</string>\n    <string name=\"filter_all\">すべて</string>\n    <string name=\"filter_with_releases\">リリースあり</string>\n    <string name=\"filter_installed\">インストール済み</string>\n    <string name=\"filter_favorites\">お気に入り</string>\n    <string name=\"sort\">並べ替え</string>\n    <string name=\"sort_recently_updated\">最近更新</string>\n    <string name=\"sort_name\">名前</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">リポジトリ</string>\n    <string name=\"repositories_plural\">リポジトリ</string>\n    <string name=\"showing_x_of_y_repositories\">%2$d件中%1$d件のリポジトリを表示</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">インストール可能なリリースを持つリポジトリがありません</string>\n    <string name=\"no_installed_repos\">インストール済みのリポジトリがありません</string>\n    <string name=\"no_favorite_repos\">お気に入りのリポジトリがありません</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">%1$sに更新</string>\n    <string name=\"has_release\">リリースあり</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d 年前</string>\n    <string name=\"time_months_ago\">%1$d ヶ月前</string>\n    <string name=\"time_days_ago\">%1$d 日前</string>\n    <string name=\"time_hours_ago\">%1$d 時間前</string>\n    <string name=\"time_minutes_ago\">%1$d 分前</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">たった今リリース</string>\n    <string name=\"released_hours_ago\">%1$d時間前にリリース</string>\n    <string name=\"released_yesterday\">昨日リリース</string>\n    <string name=\"released_days_ago\">%1$d日前にリリース</string>\n    <string name=\"released_on_date\">%1$sにリリース</string>\n\n    <string name=\"bottom_nav_home_title\">ホーム</string>\n    <string name=\"bottom_nav_search_title\">検索</string>\n    <string name=\"bottom_nav_apps_title\">アプリ</string>\n    <string name=\"bottom_nav_profile_title\">プロフィール</string>\n\n    <string name=\"forked_repository\">フォーク</string>\n\n    <string name=\"category_stable\">安定版</string>\n    <string name=\"category_pre_release\">プレリリース</string>\n    <string name=\"category_all\">すべて</string>\n    <string name=\"select_version\">バージョンを選択</string>\n    <string name=\"pre_release_badge\">プレリリース</string>\n    <string name=\"no_version_selected\">バージョン未選択</string>\n    <string name=\"versions_title\">バージョン</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">リポジトリを開く</string>\n    <string name=\"open_in_browser\">ブラウザで開く</string>\n    <string name=\"cancel_download\">ダウンロードをキャンセル</string>\n    <string name=\"show_install_options\">インストールオプションを表示</string>\n\n    <!-- Errors & states -->\n    <string name=\"no_description\">説明はありません。</string>\n    <string name=\"no_release_notes\">リリースノートはありません。</string>\n\n    <!-- Install status -->\n    <string name=\"not_available\">利用不可</string>\n    <string name=\"update_app\">アプリを更新</string>\n    <string name=\"pending_install\">インストール待ち</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Obtainiumで開く</string>\n    <string name=\"obtainium_description\">自動的にアップデートを管理</string>\n    <string name=\"inspect_with_appmanager\">AppManagerで検査</string>\n    <string name=\"appmanager_description\">権限、トラッカー、セキュリティを確認</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">アンインストール</string>\n    <string name=\"open_app\">開く</string>\n    <string name=\"downgrade_requires_uninstall\">ダウングレードにはアンインストールが必要</string>\n    <string name=\"downgrade_warning_message\">バージョン%1$sのインストールには、現在のバージョン（%2$s）のアンインストールが必要です。アプリデータは失われます。</string>\n    <string name=\"uninstall_first\">先にアンインストール</string>\n    <string name=\"install_version\">%1$sをインストール</string>\n    <string name=\"failed_to_open_app\">%1$sを開けませんでした</string>\n    <string name=\"failed_to_uninstall\">%1$sのアンインストールに失敗しました</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">最新</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">最終確認: %1$s</string>\n    <string name=\"last_checked_never\">未確認</string>\n    <string name=\"last_checked_just_now\">たった今</string>\n    <string name=\"last_checked_minutes_ago\">%1$d分前</string>\n    <string name=\"last_checked_hours_ago\">%1$d時間前</string>\n    <string name=\"checking_for_updates\">アップデートを確認中…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">プロキシの種類</string>\n    <string name=\"proxy_none\">なし</string>\n    <string name=\"proxy_system\">システム</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">ホスト</string>\n    <string name=\"proxy_port\">ポート</string>\n    <string name=\"proxy_username\">ユーザー名（任意）</string>\n    <string name=\"proxy_password\">パスワード（任意）</string>\n    <string name=\"proxy_save\">プロキシを保存</string>\n    <string name=\"proxy_saved\">プロキシ設定を保存しました</string>\n    <string name=\"proxy_system_description\">デバイスのプロキシ設定を使用します</string>\n    <string name=\"proxy_port_error\">ポートは1〜65535の範囲で指定してください</string>\n    <string name=\"proxy_none_description\">直接接続、プロキシなし</string>\n    <string name=\"failed_to_save_proxy_settings\">プロキシ設定の保存に失敗しました</string>\n    <string name=\"proxy_host_required\">プロキシホストは必須です</string>\n    <string name=\"invalid_proxy_port\">無効なプロキシポート</string>\n    <string name=\"proxy_show_password\">パスワードを表示</string>\n    <string name=\"proxy_hide_password\">パスワードを非表示</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">このアプリを追跡</string>\n    <string name=\"app_tracked_successfully\">アプリを追跡リストに追加しました</string>\n    <string name=\"failed_to_track_app\">アプリの追跡に失敗しました: %1$s</string>\n    <string name=\"already_tracked\">このアプリは既に追跡中です</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">GitHubにサインイン</string>\n    <string name=\"profile_sign_in_description\">すべての機能を解放しましょう。アプリを管理し、設定を同期し、より速く閲覧できます。</string>\n    <string name=\"profile_repos\">リポジトリ</string>\n    <string name=\"profile_login\">ログイン</string>\n    <string name=\"profile_stars_description\">GitHubのスター付きリポジトリ</string>\n    <string name=\"profile_favourites_description\">ローカルに保存されたお気に入りリポジトリ</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">セッション期限切れ</string>\n    <string name=\"session_expired_message\">GitHubセッションの有効期限が切れたか、トークンが取り消されました。認証機能を引き続き使用するには、再度サインインしてください。</string>\n    <string name=\"session_expired_hint\">制限されたAPIリクエストでゲストとして閲覧を続けることができます。</string>\n    <string name=\"sign_in_again\">再度サインイン</string>\n    <string name=\"continue_as_guest\">ゲストとして続行</string>\n    <string name=\"logout_revocation_note\">ローカルセッションとキャッシュデータが消去されます。アクセスを完全に取り消すには、GitHub Settings > Applicationsにアクセスしてください。</string>\n    <string name=\"auth_code_expires_in\">コードの有効期限: %1$s</string>\n    <string name=\"auth_error_code_expired\">デバイスコードの有効期限が切れました。</string>\n    <string name=\"auth_hint_try_again\">新しいコードを取得するために再度サインインしてください。</string>\n    <string name=\"auth_hint_check_connection\">インターネット接続を確認して再試行してください。</string>\n    <string name=\"auth_hint_denied\">認証リクエストを拒否しました。意図しない場合は再試行してください。</string>\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">もっと読む</string>\n    <string name=\"show_less\">折りたたむ</string>\n\n    <string name=\"share_repository\">リポジトリを共有</string>\n    <string name=\"failed_to_share_link\">リンクの共有に失敗しました</string>\n    <string name=\"link_copied_to_clipboard\">リンクをクリップボードにコピーしました</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">翻訳</string>\n    <string name=\"translating\">翻訳中…</string>\n    <string name=\"show_original\">原文を表示</string>\n    <string name=\"translated_to\">%1$sに翻訳済み</string>\n    <string name=\"translate_to\">翻訳先…</string>\n    <string name=\"search_language\">言語を検索</string>\n    <string name=\"change_language\">言語を変更</string>\n    <string name=\"translation_failed\">翻訳に失敗しました。もう一度お試しください。</string>\n\n    <string name=\"open_github_link\">GitHubリンクを開く</string>\n    <string name=\"clipboard_link_detected\">クリップボードにGitHubリンクを検出</string>\n    <string name=\"auto_detect_clipboard_links\">クリップボードリンクの自動検出</string>\n    <string name=\"auto_detect_clipboard_description\">検索画面を開く際にクリップボードからGitHubリンクを自動検出</string>\n    <string name=\"detected_links\">検出されたリンク</string>\n    <string name=\"open_in_app\">アプリで開く</string>\n    <string name=\"no_github_link_in_clipboard\">クリップボードにGitHubリンクが見つかりません</string>\n\n    <string name=\"storage\">ストレージ</string>\n    <string name=\"clear_cache\">キャッシュをクリア</string>\n    <string name=\"current_size\">現在のサイズ:</string>\n    <string name=\"clear\">クリア</string>\n\n    <string name=\"sponsor_title\">GitHub Store を支援</string>\n    <string name=\"sponsor_button\">プロジェクトを支援</string>\n    <string name=\"sponsor_hero_title\">愛を込めて作り、\\nコーヒーで維持</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store は 13万以上のダウンロードと 7700 以上の GitHub スターを達成しました。完全無料、広告なし、追跡なし。</string>\n\n    <string name=\"sponsor_personal_note\">私は高校を卒業しながら、このプロジェクトを一人で開発・維持しています。小さな支援でも、バグ修正やインフラ費用、機能開発に役立ちます。</string>\n\n    <string name=\"sponsor_kodee_title\">GitHub Store に投票！</string>\n    <string name=\"sponsor_kodee_subtitle\">KotlinConf 2026 の Golden Kodee Awards にノミネートされています。投票は2分で完了します。</string>\n\n    <string name=\"sponsor_kodee_register\">1. 登録</string>\n    <string name=\"sponsor_kodee_vote\">2. 投票</string>\n    <string name=\"sponsor_kodee_deadline\">投票締切：3月22日</string>\n\n    <string name=\"sponsor_kodee_step1\">1. 賞のプラットフォームに登録（Googleで続行）</string>\n    <string name=\"sponsor_kodee_step2\">2. 下の「投票」をタップ</string>\n    <string name=\"sponsor_kodee_step3\">3. Usmon Narzullayev を見つけて投票</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">GitHub 経由の定期または一回支援</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">簡単な一回支援</string>\n\n    <string name=\"sponsor_other_ways_title\">他の支援方法</string>\n\n    <string name=\"sponsor_star_repo\">リポジトリにスター</string>\n    <string name=\"sponsor_star_repo_desc\">他の人が GitHub Store を見つけやすくなります</string>\n\n    <string name=\"sponsor_report_bugs\">バグを報告</string>\n    <string name=\"sponsor_report_bugs_desc\">アプリをより良くします</string>\n\n    <string name=\"sponsor_share\">友達と共有</string>\n    <string name=\"sponsor_share_desc\">他の開発者に広める</string>\n\n    <string name=\"sponsor_thank_you\">金銭的であれそうでなくても、すべての支援がこのプロジェクトを支えています。ありがとうございます！</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">インストール</string>\n    <string name=\"installer_type_default\">デフォルト</string>\n    <string name=\"installer_type_default_description\">標準のシステムインストールダイアログ</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">確認なしのサイレントインストール</string>\n    <string name=\"shizuku_status_not_installed\">Shizukuがインストールされていません</string>\n    <string name=\"shizuku_status_not_running\">Shizukuが実行されていません</string>\n    <string name=\"shizuku_status_permission_needed\">権限が必要です</string>\n    <string name=\"shizuku_status_ready\">準備完了</string>\n    <string name=\"shizuku_grant_permission\">権限を付与</string>\n    <string name=\"shizuku_install_hint\">サイレントインストールを有効にするにはShizukuをインストールしてください</string>\n    <string name=\"shizuku_start_hint\">サイレントインストールを有効にするにはShizukuを起動してください</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizukuインストールに失敗、標準インストーラーを使用します</string>\n\n    <string name=\"auto_update_title\">アプリを自動更新</string>\n    <string name=\"auto_update_description\">Shizukuを使用してバックグラウンドで自動的にアップデートをダウンロードしてインストール</string>\n\n    <string name=\"section_updates\">アップデート</string>\n    <string name=\"update_check_interval_title\">アップデート確認間隔</string>\n    <string name=\"update_check_interval_description\">バックグラウンドでアプリのアップデートを確認する頻度</string>\n    <string name=\"interval_3h\">3時間</string>\n    <string name=\"interval_6h\">6時間</string>\n    <string name=\"interval_12h\">12時間</string>\n    <string name=\"interval_24h\">24時間</string>\n\n    <string name=\"add_by_link\">リンクで追加</string>\n    <string name=\"link_app_title\">アプリをリポジトリにリンク</string>\n    <string name=\"pick_installed_app\">GitHubリポジトリにリンクするインストール済みアプリを選択</string>\n    <string name=\"search_apps_hint\">アプリを検索…</string>\n    <string name=\"enter_repo_url\">GitHubリポジトリURL</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">検証中…</string>\n    <string name=\"link_and_track\">リンクして追跡</string>\n    <string name=\"checking_release\">最新リリースを確認中…</string>\n    <string name=\"downloading_for_verification\">検証用APKをダウンロード中…</string>\n    <string name=\"verifying_signing_key\">署名キーを検証中…</string>\n    <string name=\"package_name_mismatch\">パッケージ名が一致しません：APKは%1$sですが、選択されたアプリは%2$sです</string>\n    <string name=\"signing_key_mismatch_link\">署名キーが一致しません：このリポジトリのAPKは別の開発者によって署名されています</string>\n    <string name=\"select_asset_title\">インストーラーを選択</string>\n    <string name=\"select_asset_description\">インストール済みアプリと照合するAPKを選択</string>\n    <string name=\"download_failed\">ダウンロード失敗</string>\n    <string name=\"export_apps\">エクスポート</string>\n    <string name=\"import_apps\">インポート</string>\n    <string name=\"import_apps_title\">アプリをインポート</string>\n    <string name=\"import_apps_description\">エクスポートしたJSONを貼り付けて追跡中のアプリを復元</string>\n    <string name=\"import_apps_hint\">エクスポートしたJSONをここに貼り付け…</string>\n    <string name=\"include_pre_releases_title\">プレリリースを含める</string>\n    <string name=\"include_pre_releases_description\">アップデート確認時にプレリリース版を追跡します。無効の場合、安定版リリースのみが対象となります。</string>\n    <string name=\"confirm_uninstall_title\">アプリをアンインストールしますか？</string>\n    <string name=\"confirm_uninstall_message\">%1$sをアンインストールしてよろしいですか？この操作は元に戻せず、アプリのデータが失われる可能性があります。</string>\n    <string name=\"invalid_github_url\">無効なGitHub URL。形式を使用してください：github.com/owner/repo</string>\n    <string name=\"repo_not_found\">リポジトリが見つかりません：%1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub APIのレート制限を超えました。後でもう一度お試しください。</string>\n    <string name=\"failed_to_link\">リンクに失敗：%1$s</string>\n    <string name=\"failed_to_load_apps\">インストール済みアプリの読み込みに失敗</string>\n    <string name=\"app_linked_success\">%1$sを%2$s/%3$sにリンクしました</string>\n    <string name=\"export_failed\">エクスポート失敗：%1$s</string>\n    <string name=\"import_failed\">インポート失敗：%1$s</string>\n    <string name=\"imported_apps_summary\">%1$d個のアプリをインポート</string>\n    <string name=\"imported_skipped\">、%1$d個スキップ</string>\n    <string name=\"imported_failed\">、%1$d個失敗</string>\n    <string name=\"signing_key_changed_title\">署名キーが変更されました</string>\n    <string name=\"signing_key_changed_message\">このアプリの署名証明書が初回インストール以降に変更されました。\\n\\nこれは開発者が署名キーを変更したか、バイナリが改ざんされた可能性があります。\\n\\n期待値：%1$s\\n受信値：%2$s</string>\n    <string name=\"install_anyway\">それでもインストール</string>\n    <string name=\"verified_build\">検証済みビルド</string>\n    <string name=\"checking_attestation\">確認中\\u2026</string>\n    <string name=\"assets_title\">アセット</string>\n    <string name=\"no_assets_selected\">アセットなし</string>\n    <string name=\"no_assets_in_list\">このリリースに関連するアセットはありません</string>\n    <string name=\"assets_selection_label\">アセットオプションを選択</string>\n    <string name=\"multiple_assets_info_dialog_title\">複数のアセットが利用可能</string>\n    <string name=\"multiple_assets_info_dialog_text\">このリリースには複数のインストール可能なファイルがあります。リストを確認し、お使いのデバイスに合ったものを選択してください。</string>\n    <string name=\"icon_content_description_info\">情報</string>\n    <string name=\"translation_error_retry\">再試行</string>\n    <string name=\"translated_from\">自動検出：%1$s</string>\n    <string name=\"select_language\">言語を選択</string>\n    <string name=\"update_package_mismatch\">パッケージの不一致: APKは%1$sですが、インストール済みアプリは%2$sです。更新がブロックされました。</string>\n    <string name=\"update_signing_key_mismatch\">署名キーの不一致: 更新は別の開発者によって署名されています。更新がブロックされました。</string>\n    <string name=\"liquid_glass_option_title\">リキッドグラスエフェクト</string>\n    <string name=\"liquid_glass_option_description\">滑らかなガラスのような外観でインターフェースを向上させます</string>\n\n    <string name=\"hide_seen_title\">閲覧済みリポジトリを非表示</string>\n    <string name=\"hide_seen_description\">すでに閲覧したリポジトリをディスカバリーフィードから非表示にします</string>\n    <string name=\"clear_seen_history\">閲覧履歴をクリア</string>\n    <string name=\"clear_seen_history_description\">すべての閲覧済みリポジトリをリセットしてフィードに再表示します</string>\n    <string name=\"seen_history_cleared\">閲覧履歴をクリアしました</string>\n    <string name=\"seen_badge\">閲覧済み</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">설치된 앱</string>\n    <string name=\"navigate_back\">뒤로 이동</string>\n    <string name=\"check_for_updates\">업데이트 확인</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">%1$s을(를) 실행할 수 없습니다</string>\n    <string name=\"failed_to_open\">%1$s을(를) 열지 못했습니다</string>\n    <string name=\"failed_to_update\">%1$s 업데이트 실패: %2$s</string>\n    <string name=\"update_failed\">업데이트 실패</string>\n    <string name=\"update_all_failed\">모두 업데이트 실패: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">모든 앱이 성공적으로 업데이트되었습니다</string>\n    <string name=\"no_updates_available\">사용 가능한 업데이트가 없습니다</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">앱 검색</string>\n    <string name=\"no_apps_found\">앱을 찾을 수 없습니다</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">모두 업데이트</string>\n    <string name=\"update\">업데이트</string>\n    <string name=\"open\">열기</string>\n    <string name=\"cancel\">취소</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">확인 중…</string>\n    <string name=\"updated_successfully\">성공적으로 업데이트됨</string>\n    <string name=\"error_with_message\">오류: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">%2$d개 중 %1$d개 업데이트 중</string>\n    <string name=\"currently_updating\">현재 업데이트 중: %1$s</string>\n\n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">인증 대기 중…</string>\n    <string name=\"signed_in\">로그인 완료!</string>\n    <string name=\"redirecting_message\">이제 앱을 사용할 수 있습니다. 이동 중…</string>\n    <string name=\"try_again\">다시 시도</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">오류: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">GitHub에 다음 코드를 입력하세요:</string>\n    <string name=\"copy_code\">코드 복사</string>\n    <string name=\"open_github\">GitHub 열기</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">전체\\n기능 잠금 해제</string>\n\n    <string name=\"more_requests\">더 많은 요청</string>\n    <string name=\"more_requests_description\">\n        로그인하여 API 요청 한도를 늘리고 중단 없이 사용하세요.\n    </string>\n\n    <string name=\"sign_in_with_github\">GitHub로 로그인</string>\n\n    <string name=\"error_cancelled\">취소됨</string>\n    <string name=\"error_unknown\">알 수 없는 오류</string>\n\n    <string name=\"language_label\">언어:</string>\n\n    <string name=\"discover_repositories\">저장소 탐색</string>\n    <string name=\"search_repositories_hint\">저장소, 설명 검색…</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">언어로 필터링</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">%1$d개의 결과를 찾았습니다</string>\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">정렬 기준</string>\n    <string name=\"close\">닫기</string>\n\n    <string name=\"sort_most_stars\">별 많은 순</string>\n    <string name=\"sort_most_forks\">포크 많은 순</string>\n    <string name=\"sort_best_match\">최적의 결과</string>\n\n    <string name=\"sort_order_descending\">내림차순</string>\n    <string name=\"sort_order_ascending\">오름차순</string>\n    <string name=\"sort_label\">정렬</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">모든 언어</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">검색 실패</string>\n    <string name=\"no_repositories_found\">저장소를 찾을 수 없습니다</string>\n\n    <!-- Settings -->\n    <string name=\"profile_title\">프로필</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">외관</string>\n    <string name=\"section_about\">정보</string>\n    <string name=\"section_network\">네트워크</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">테마 색상</string>\n    <string name=\"amoled_black_theme\">AMOLED 블랙 테마</string>\n    <string name=\"amoled_black_description\">다크 모드용 순수 블랙 배경</string>\n    <string name=\"selected_color\">선택된 색상: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">버전</string>\n    <string name=\"help_support\">도움말 및 지원</string>\n\n    <!-- Account -->\n    <string name=\"logout\">로그아웃</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">성공적으로 로그아웃되었습니다. 이동 중...</string>\n    <string name=\"cache_cleared\">캐시가 성공적으로 삭제되었습니다</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">경고!</string>\n    <string name=\"logout_confirmation\">정말 로그아웃하시겠습니까?</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">동적</string>\n    <string name=\"theme_ocean\">오션</string>\n    <string name=\"theme_purple\">퍼플</string>\n    <string name=\"theme_forest\">포레스트</string>\n    <string name=\"theme_slate\">슬레이트</string>\n    <string name=\"theme_amber\">앰버</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">저장소 열기</string>\n    <string name=\"open_in_browser\">브라우저에서 열기</string>\n    <string name=\"cancel_download\">다운로드 취소</string>\n    <string name=\"show_install_options\">설치 옵션 보기</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">상세 정보를 불러오는 중 오류 발생</string>\n    <string name=\"retry\">다시 시도</string>\n    <string name=\"no_description\">설명이 없습니다.</string>\n    <string name=\"no_release_notes\">릴리스 노트가 없습니다.</string>\n    <string name=\"report_issue\">문제 신고</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">이 앱 정보</string>\n    <string name=\"install_logs\">설치 로그</string>\n    <string name=\"author\">작성자</string>\n    <string name=\"whats_new\">새로운 기능</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">설치됨</string>\n    <string name=\"update_available\">업데이트 가능</string>\n    <string name=\"not_available\">사용 불가</string>\n    <string name=\"install_latest\">최신 버전 설치</string>\n    <string name=\"reinstall\">재설치</string>\n    <string name=\"update_app\">앱 업데이트</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">다운로드 중</string>\n    <string name=\"updating\">업데이트 중</string>\n    <string name=\"verifying\">검증 중</string>\n    <string name=\"installing\">설치 중</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Obtainium에서 열기</string>\n    <string name=\"obtainium_description\">업데이트 자동 관리</string>\n    <string name=\"inspect_with_appmanager\">AppManager로 검사</string>\n    <string name=\"appmanager_description\">권한, 트래커 및 보안 확인</string>\n\n    <!-- Author -->\n    <string name=\"profile\">프로필</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">포크</string>\n    <string name=\"stars\">별</string>\n    <string name=\"issues\">이슈</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">%1$s 작성</string>\n    <string name=\"installed_version\">• 설치됨: %1$s</string>\n    <string name=\"architecture_compatible\">아키텍처 호환</string>\n    <string name=\"update_to_version\">%1$s(으)로 업데이트</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">상세 정보를 불러오지 못했습니다</string>\n    <string name=\"installer_saved_downloads\">설치 파일이 다운로드 폴더에 저장되었습니다</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">다운로드 시작됨</string>\n    <string name=\"log_downloaded\">다운로드 완료</string>\n    <string name=\"log_update_started\">업데이트 시작됨</string>\n    <string name=\"log_installed\">설치됨</string>\n    <string name=\"log_updated\">업데이트됨</string>\n    <string name=\"log_cancelled\">취소됨</string>\n    <string name=\"log_install_started\">설치 시작됨</string>\n    <string name=\"log_error\">오류</string>\n    <string name=\"log_error_with_message\">오류: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">AppManager 준비 중</string>\n    <string name=\"log_opened_appmanager\">AppManager에서 열림</string>\n    <string name=\"log_permission_blocked\">기기 정책에 의해 설치 권한이 차단됨</string>\n    <string name=\"log_opened_external_installer\">외부 설치 프로그램에서 열림</string>\n    <string name=\"install_permission_unavailable\">설치 권한을 사용할 수 없음</string>\n    <string name=\"install_permission_blocked_message\">APK가 성공적으로 다운로드되었지만 이 기기에서는 직접 설치가 허용되지 않습니다. 외부 설치 프로그램으로 열까요?</string>\n    <string name=\"open_with_external_installer\">외부 설치 프로그램으로 열기</string>\n    <string name=\"external_installer_description\">타사 앱을 사용하여 APK 설치</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">오류: %1$s</string>\n    <string name=\"error_asset_not_supported\">.%1$s 파일 형식은 지원되지 않습니다</string>\n    <string name=\"error_file_not_found\">다운로드된 파일을 찾을 수 없습니다</string>\n\n    <string name=\"home_category_trending\">인기</string>\n    <string name=\"home_category_hot_release\">핫 릴리스</string>\n    <string name=\"home_category_most_popular\">가장 인기 있는</string>\n\n    <string name=\"home_finding_repositories\">저장소를 찾는 중...</string>\n    <string name=\"home_loading_more\">더 불러오는 중...</string>\n    <string name=\"home_no_more_repositories\">더 이상 저장소가 없습니다</string>\n    <string name=\"home_retry\">다시 시도</string>\n    <string name=\"home_failed_to_load_repositories\">저장소를 불러오지 못했습니다</string>\n    <string name=\"home_view_details\">상세 보기</string>\n\n    <string name=\"updated_just_now\">방금 업데이트됨</string>\n    <string name=\"updated_hours_ago\">%1$d시간 전 업데이트됨</string>\n    <string name=\"updated_yesterday\">어제 업데이트됨</string>\n    <string name=\"updated_days_ago\">%1$d일 전 업데이트됨</string>\n    <string name=\"updated_on_date\">%1$s에 업데이트됨</string>\n\n    <string name=\"rate_limit_exceeded\">요청 한도 초과</string>\n    <string name=\"rate_limit_used_all\">%1$d개의 API 요청을 모두 사용했습니다.</string>\n    <string name=\"rate_limit_used_all_free\">%1$d개의 무료 API 요청을 모두 사용했습니다.</string>\n    <string name=\"rate_limit_resets_in_minutes\">%1$d분 후 초기화</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 로그인하면 시간당 60회 대신 5,000회 요청을 사용할 수 있습니다!</string>\n    <string name=\"rate_limit_sign_in\">로그인</string>\n    <string name=\"rate_limit_ok\">확인</string>\n    <string name=\"rate_limit_close\">닫기</string>\n\n    <string name=\"system_font\">시스템 글꼴</string>\n    <string name=\"system_font_description\">가독성을 위해 기기 글꼴과 일치</string>\n\n    <string name=\"theme_light\">라이트</string>\n    <string name=\"theme_dark\">다크</string>\n    <string name=\"theme_system\">시스템</string>\n\n    <string name=\"added_to_favourites\">리포지토리를 즐겨찾기에 추가했습니다</string>\n    <string name=\"removed_from_favourites\">리포지토리를 즐겨찾기에서 제거했습니다</string>\n    <string name=\"add_to_favourites\">즐겨찾기에 추가</string>\n    <string name=\"remove_from_favourites\">즐겨찾기에서 제거</string>\n    <string name=\"favourites\">즐겨찾기</string>\n\n    <string name=\"added_just_now\">방금 추가됨</string>\n    <string name=\"added_hours_ago\">%1$d시간 전에 추가됨</string>\n    <string name=\"added_yesterday\">어제 추가됨</string>\n    <string name=\"added_days_ago\">%1$d일 전에 추가됨</string>\n    <string name=\"added_on_date\">%1$s에 추가됨</string>\n\n    <string name=\"starred_repositories\">별표 표시된 저장소</string>\n    <string name=\"repository_starred\">저장소에 별표가 추가되었습니다</string>\n    <string name=\"repository_not_starred\">저장소에 별표가 없습니다</string>\n    <string name=\"star_from_github\">GitHub에서 저장소에 별표를 추가할 수 있습니다</string>\n    <string name=\"unstar_from_github\">GitHub에서 별표를 제거할 수 있습니다</string>\n\n    <string name=\"sign_in_required\">로그인이 필요합니다</string>\n    <string name=\"no_starred_repos\">별표 표시된 저장소가 없습니다</string>\n    <string name=\"sign_in_with_github_for_stars\">GitHub로 로그인하여 별표 저장소를 확인하세요</string>\n    <string name=\"star_repos_hint\">설치 가능한 릴리스가 있는 저장소에 별표를 추가하세요</string>\n    <string name=\"last_synced\">마지막 동기화</string>\n    <string name=\"just_now\">방금</string>\n    <string name=\"minutes_ago\">%1$d분 전</string>\n    <string name=\"hours_ago\">%1$d시간 전</string>\n    <string name=\"days_ago\">%1$d일 전</string>\n    <string name=\"dismiss\">닫기</string>\n    <string name=\"sync_starred_failed\">별표 저장소 동기화에 실패했습니다</string>\n\n    <string name=\"developer_profile_title\">개발자 프로필</string>\n    <string name=\"open_developer_profile\">개발자 프로필 열기</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">저장소를 불러오지 못했습니다</string>\n    <string name=\"failed_to_load_profile\">프로필을 불러오지 못했습니다</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">저장소</string>\n    <string name=\"followers\">팔로워</string>\n    <string name=\"following\">팔로잉</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">저장소 검색…</string>\n    <string name=\"clear_search\">검색 지우기</string>\n    <string name=\"filter_all\">전체</string>\n    <string name=\"filter_with_releases\">릴리스 포함</string>\n    <string name=\"filter_installed\">설치됨</string>\n    <string name=\"filter_favorites\">즐겨찾기</string>\n    <string name=\"sort\">정렬</string>\n    <string name=\"sort_recently_updated\">최근 업데이트</string>\n    <string name=\"sort_name\">이름</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">저장소</string>\n    <string name=\"repositories_plural\">저장소</string>\n    <string name=\"showing_x_of_y_repositories\">총 %2$d개 중 %1$d개의 저장소 표시</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">설치 가능한 릴리스가 있는 저장소가 없습니다</string>\n    <string name=\"no_installed_repos\">설치된 저장소가 없습니다</string>\n    <string name=\"no_favorite_repos\">즐겨찾기 저장소가 없습니다</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">%1$s 업데이트됨</string>\n    <string name=\"has_release\">릴리스 있음</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d 년 전</string>\n    <string name=\"time_months_ago\">%1$d 개월 전</string>\n    <string name=\"time_days_ago\">%1$d 일 전</string>\n    <string name=\"time_hours_ago\">%1$d 시간 전</string>\n    <string name=\"time_minutes_ago\">%1$d 분 전</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">방금 출시됨</string>\n    <string name=\"released_hours_ago\">%1$d시간 전에 출시됨</string>\n    <string name=\"released_yesterday\">어제 출시됨</string>\n    <string name=\"released_days_ago\">%1$d일 전에 출시됨</string>\n    <string name=\"released_on_date\">%1$s에 출시됨</string>\n\n    <string name=\"bottom_nav_home_title\">홈</string>\n    <string name=\"bottom_nav_search_title\">검색</string>\n    <string name=\"bottom_nav_apps_title\">앱</string>\n    <string name=\"bottom_nav_profile_title\">프로필</string>\n\n    <string name=\"forked_repository\">포크</string>\n\n    <string name=\"category_stable\">안정 버전</string>\n    <string name=\"category_pre_release\">사전 출시</string>\n    <string name=\"category_all\">전체</string>\n    <string name=\"select_version\">버전 선택</string>\n    <string name=\"pre_release_badge\">사전 출시</string>\n    <string name=\"no_version_selected\">선택된 버전 없음</string>\n    <string name=\"versions_title\">버전</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">설치 대기 중</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">제거</string>\n    <string name=\"open_app\">열기</string>\n    <string name=\"downgrade_requires_uninstall\">다운그레이드를 위해 제거가 필요합니다</string>\n    <string name=\"downgrade_warning_message\">버전 %1$s을(를) 설치하려면 현재 버전(%2$s)을 먼저 제거해야 합니다. 앱 데이터가 삭제됩니다.</string>\n    <string name=\"uninstall_first\">먼저 제거</string>\n    <string name=\"install_version\">%1$s 설치</string>\n    <string name=\"failed_to_open_app\">%1$s 열기 실패</string>\n    <string name=\"failed_to_uninstall\">%1$s 제거 실패</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">최신</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">마지막 확인: %1$s</string>\n    <string name=\"last_checked_never\">확인한 적 없음</string>\n    <string name=\"last_checked_just_now\">방금</string>\n    <string name=\"last_checked_minutes_ago\">%1$d분 전</string>\n    <string name=\"last_checked_hours_ago\">%1$d시간 전</string>\n    <string name=\"checking_for_updates\">업데이트 확인 중…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">프록시 유형</string>\n    <string name=\"proxy_none\">없음</string>\n    <string name=\"proxy_system\">시스템</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">호스트</string>\n    <string name=\"proxy_port\">포트</string>\n    <string name=\"proxy_username\">사용자 이름 (선택 사항)</string>\n    <string name=\"proxy_password\">비밀번호 (선택 사항)</string>\n    <string name=\"proxy_save\">프록시 저장</string>\n    <string name=\"proxy_saved\">프록시 설정이 저장되었습니다</string>\n    <string name=\"proxy_system_description\">기기의 프록시 설정을 사용합니다</string>\n    <string name=\"proxy_port_error\">포트는 1–65535 사이여야 합니다</string>\n    <string name=\"proxy_none_description\">직접 연결, 프록시 없음</string>\n    <string name=\"failed_to_save_proxy_settings\">프록시 설정을 저장하지 못했습니다</string>\n    <string name=\"proxy_host_required\">프록시 호스트가 필요합니다</string>\n    <string name=\"invalid_proxy_port\">잘못된 프록시 포트</string>\n    <string name=\"proxy_show_password\">비밀번호 표시</string>\n    <string name=\"proxy_hide_password\">비밀번호 숨기기</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">이 앱 추적</string>\n    <string name=\"app_tracked_successfully\">앱이 추적 목록에 추가되었습니다</string>\n    <string name=\"failed_to_track_app\">앱 추적 실패: %1$s</string>\n    <string name=\"already_tracked\">이미 추적 중인 앱입니다</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">GitHub에 로그인</string>\n    <string name=\"profile_sign_in_description\">전체 기능을 잠금 해제하세요. 앱을 관리하고, 설정을 동기화하고, 더 빠르게 탐색하세요.</string>\n    <string name=\"profile_repos\">저장소</string>\n    <string name=\"profile_login\">로그인</string>\n    <string name=\"profile_stars_description\">GitHub에서 별표한 저장소</string>\n    <string name=\"profile_favourites_description\">로컬에 저장된 즐겨찾기 저장소</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">세션 만료</string>\n    <string name=\"session_expired_message\">GitHub 세션이 만료되었거나 토큰이 취소되었습니다. 인증된 기능을 계속 사용하려면 다시 로그인하세요.</string>\n    <string name=\"session_expired_hint\">제한된 API 요청으로 게스트로 계속 탐색할 수 있습니다.</string>\n    <string name=\"sign_in_again\">다시 로그인</string>\n    <string name=\"continue_as_guest\">게스트로 계속</string>\n    <string name=\"logout_revocation_note\">로컬 세션과 캐시 데이터가 삭제됩니다. 접근을 완전히 취소하려면 GitHub Settings > Applications를 방문하세요.</string>\n    <string name=\"auth_code_expires_in\">코드 만료까지 %1$s</string>\n    <string name=\"auth_error_code_expired\">디바이스 코드가 만료되었습니다.</string>\n    <string name=\"auth_hint_try_again\">새 코드를 받으려면 다시 로그인해 주세요.</string>\n    <string name=\"auth_hint_check_connection\">인터넷 연결을 확인하고 다시 시도하세요.</string>\n    <string name=\"auth_hint_denied\">인증 요청을 거부했습니다. 의도하지 않은 경우 다시 시도하세요.</string>\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">더 보기</string>\n    <string name=\"show_less\">간략히</string>\n\n    <string name=\"share_repository\">저장소 공유</string>\n    <string name=\"failed_to_share_link\">링크 공유에 실패했습니다</string>\n    <string name=\"link_copied_to_clipboard\">링크가 클립보드에 복사되었습니다</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">번역</string>\n    <string name=\"translating\">번역 중…</string>\n    <string name=\"show_original\">원문 보기</string>\n    <string name=\"translated_to\">%1$s로 번역됨</string>\n    <string name=\"translate_to\">번역 대상…</string>\n    <string name=\"search_language\">언어 검색</string>\n    <string name=\"change_language\">언어 변경</string>\n    <string name=\"translation_failed\">번역 실패. 다시 시도해주세요.</string>\n\n    <string name=\"open_github_link\">GitHub 링크 열기</string>\n    <string name=\"clipboard_link_detected\">클립보드에서 GitHub 링크 감지됨</string>\n    <string name=\"auto_detect_clipboard_links\">클립보드 링크 자동 감지</string>\n    <string name=\"auto_detect_clipboard_description\">검색을 열 때 클립보드에서 GitHub 링크를 자동으로 감지</string>\n    <string name=\"detected_links\">감지된 링크</string>\n    <string name=\"open_in_app\">앱에서 열기</string>\n    <string name=\"no_github_link_in_clipboard\">클립보드에서 GitHub 링크를 찾을 수 없습니다</string>\n\n    <string name=\"storage\">저장 공간</string>\n    <string name=\"clear_cache\">캐시 지우기</string>\n    <string name=\"current_size\">현재 크기:</string>\n    <string name=\"clear\">지우기</string>\n\n    <string name=\"sponsor_title\">GitHub Store 지원</string>\n    <string name=\"sponsor_button\">프로젝트 지원하기</string>\n    <string name=\"sponsor_hero_title\">사랑으로 만들고,\\n커피로 유지합니다</string>\n\n    <string name=\"sponsor_hero_subtitle\">GitHub Store는 130,000+ 다운로드와 7,700+ GitHub 스타를 달성했습니다 — 100% 무료, 광고 없음, 추적 없음.</string>\n\n    <string name=\"sponsor_personal_note\">저는 고등학교를 마치면서 이 프로젝트를 혼자 개발하고 유지하고 있습니다.</string>\n\n    <string name=\"sponsor_kodee_title\">GitHub Store에 투표하세요!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store가 KotlinConf 2026 Golden Kodee Awards 후보에 올랐습니다.</string>\n\n    <string name=\"sponsor_kodee_register\">1. 등록</string>\n    <string name=\"sponsor_kodee_vote\">2. 투표</string>\n    <string name=\"sponsor_kodee_deadline\">투표 마감: 3월 22일</string>\n\n    <string name=\"sponsor_kodee_step1\">1. 플랫폼에 등록 (Google로 계속)</string>\n    <string name=\"sponsor_kodee_step2\">2. 아래에서 투표 버튼 누르기</string>\n    <string name=\"sponsor_kodee_step3\">3. Usmon Narzullayev를 찾아 투표 클릭</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">GitHub를 통한 정기 또는 일회 지원</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">빠른 일회 지원</string>\n\n    <string name=\"sponsor_other_ways_title\">다른 도움 방법</string>\n\n    <string name=\"sponsor_star_repo\">저장소에 스타 주기</string>\n    <string name=\"sponsor_star_repo_desc\">다른 사람들이 GitHub Store를 발견하도록 도움</string>\n\n    <string name=\"sponsor_report_bugs\">버그 신고</string>\n    <string name=\"sponsor_report_bugs_desc\">앱을 더 좋게 만듭니다</string>\n\n    <string name=\"sponsor_share\">친구와 공유</string>\n    <string name=\"sponsor_share_desc\">다른 개발자에게 알려주세요</string>\n\n    <string name=\"sponsor_thank_you\">금전적이든 아니든 모든 지원이 이 프로젝트를 계속 유지하게 합니다. 감사합니다!</string>\n\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">설치</string>\n    <string name=\"installer_type_default\">기본</string>\n    <string name=\"installer_type_default_description\">표준 시스템 설치 대화 상자</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">확인 없이 자동 설치</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku가 설치되지 않았습니다</string>\n    <string name=\"shizuku_status_not_running\">Shizuku가 실행되고 있지 않습니다</string>\n    <string name=\"shizuku_status_permission_needed\">권한 필요</string>\n    <string name=\"shizuku_status_ready\">준비됨</string>\n    <string name=\"shizuku_grant_permission\">권한 부여</string>\n    <string name=\"shizuku_install_hint\">자동 설치를 활성화하려면 Shizuku를 설치하세요</string>\n    <string name=\"shizuku_start_hint\">자동 설치를 활성화하려면 Shizuku를 시작하세요</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizuku 설치 실패, 표준 설치 프로그램 사용</string>\n\n    <string name=\"auto_update_title\">앱 자동 업데이트</string>\n    <string name=\"auto_update_description\">Shizuku를 통해 백그라운드에서 자동으로 업데이트 다운로드 및 설치</string>\n\n    <string name=\"section_updates\">업데이트</string>\n    <string name=\"update_check_interval_title\">업데이트 확인 주기</string>\n    <string name=\"update_check_interval_description\">백그라운드에서 앱 업데이트를 확인하는 빈도</string>\n    <string name=\"interval_3h\">3시간</string>\n    <string name=\"interval_6h\">6시간</string>\n    <string name=\"interval_12h\">12시간</string>\n    <string name=\"interval_24h\">24시간</string>\n\n    <string name=\"add_by_link\">링크로 추가</string>\n    <string name=\"link_app_title\">앱을 저장소에 연결</string>\n    <string name=\"pick_installed_app\">GitHub 저장소에 연결할 설치된 앱을 선택하세요</string>\n    <string name=\"search_apps_hint\">앱 검색…</string>\n    <string name=\"enter_repo_url\">GitHub 저장소 URL</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">확인 중…</string>\n    <string name=\"link_and_track\">연결 및 추적</string>\n    <string name=\"checking_release\">최신 릴리스 확인 중…</string>\n    <string name=\"downloading_for_verification\">확인용 APK 다운로드 중…</string>\n    <string name=\"verifying_signing_key\">서명 키 확인 중…</string>\n    <string name=\"package_name_mismatch\">패키지 이름 불일치: APK는 %1$s이지만 선택된 앱은 %2$s입니다</string>\n    <string name=\"signing_key_mismatch_link\">서명 키 불일치: 이 저장소의 APK는 다른 개발자가 서명했습니다</string>\n    <string name=\"select_asset_title\">설치 파일 선택</string>\n    <string name=\"select_asset_description\">설치된 앱과 대조할 APK를 선택하세요</string>\n    <string name=\"download_failed\">다운로드 실패</string>\n    <string name=\"export_apps\">내보내기</string>\n    <string name=\"import_apps\">가져오기</string>\n    <string name=\"import_apps_title\">앱 가져오기</string>\n    <string name=\"import_apps_description\">내보낸 JSON을 붙여넣어 추적 중인 앱을 복원하세요</string>\n    <string name=\"import_apps_hint\">내보낸 JSON을 여기에 붙여넣기…</string>\n    <string name=\"include_pre_releases_title\">사전 릴리스 포함</string>\n    <string name=\"include_pre_releases_description\">업데이트 확인 시 사전 릴리스 버전을 추적합니다. 비활성화하면 안정적인 릴리스만 고려됩니다.</string>\n    <string name=\"confirm_uninstall_title\">앱을 제거하시겠습니까?</string>\n    <string name=\"confirm_uninstall_message\">%1$s을(를) 제거하시겠습니까? 이 작업은 취소할 수 없으며 앱 데이터가 손실될 수 있습니다.</string>\n    <string name=\"invalid_github_url\">잘못된 GitHub URL입니다. 형식: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">저장소를 찾을 수 없음: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub API 요청 한도를 초과했습니다. 나중에 다시 시도하세요.</string>\n    <string name=\"failed_to_link\">연결 실패: %1$s</string>\n    <string name=\"failed_to_load_apps\">설치된 앱을 불러오지 못했습니다</string>\n    <string name=\"app_linked_success\">%1$s이(가) %2$s/%3$s에 연결됨</string>\n    <string name=\"export_failed\">내보내기 실패: %1$s</string>\n    <string name=\"import_failed\">가져오기 실패: %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d개의 앱을 가져왔습니다</string>\n    <string name=\"imported_skipped\">, %1$d개 건너뜀</string>\n    <string name=\"imported_failed\">, %1$d개 실패</string>\n    <string name=\"signing_key_changed_title\">서명 키가 변경됨</string>\n    <string name=\"signing_key_changed_message\">이 앱의 서명 인증서가 처음 설치된 이후 변경되었습니다.\\n\\n개발자가 서명 키를 교체했거나 바이너리가 변조되었을 수 있습니다.\\n\\n예상: %1$s\\n수신: %2$s</string>\n    <string name=\"install_anyway\">그래도 설치</string>\n    <string name=\"verified_build\">검증된 빌드</string>\n    <string name=\"checking_attestation\">확인 중\\u2026</string>\n    <string name=\"assets_title\">에셋</string>\n    <string name=\"no_assets_selected\">에셋 없음</string>\n    <string name=\"no_assets_in_list\">이 릴리스에 연관된 에셋이 없습니다</string>\n    <string name=\"assets_selection_label\">에셋 옵션 선택</string>\n    <string name=\"multiple_assets_info_dialog_title\">여러 에셋 사용 가능</string>\n    <string name=\"multiple_assets_info_dialog_text\">이 릴리스에 여러 설치 가능한 파일이 있습니다. 목록을 검토하고 기기에 맞는 파일을 선택하세요.</string>\n    <string name=\"icon_content_description_info\">정보</string>\n    <string name=\"translation_error_retry\">재시도</string>\n    <string name=\"translated_from\">자동 감지: %1$s</string>\n    <string name=\"select_language\">언어 선택</string>\n    <string name=\"update_package_mismatch\">패키지 불일치: APK는 %1$s이지만 설치된 앱은 %2$s입니다. 업데이트가 차단되었습니다.</string>\n    <string name=\"update_signing_key_mismatch\">서명 키 불일치: 업데이트가 다른 개발자에 의해 서명되었습니다. 업데이트가 차단되었습니다.</string>\n    <string name=\"liquid_glass_option_title\">리퀴드 글라스 효과</string>\n    <string name=\"liquid_glass_option_description\">매끄러운 유리 같은 외관으로 인터페이스를 향상시킵니다</string>\n\n    <string name=\"hide_seen_title\">본 저장소 숨기기</string>\n    <string name=\"hide_seen_description\">이미 본 저장소를 디스커버리 피드에서 숨깁니다</string>\n    <string name=\"clear_seen_history\">조회 기록 삭제</string>\n    <string name=\"clear_seen_history_description\">모든 조회 기록을 초기화하여 피드에 다시 표시합니다</string>\n    <string name=\"seen_history_cleared\">조회 기록이 삭제되었습니다</string>\n    <string name=\"seen_badge\">확인함</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml",
    "content": "<resources>\n\n    <string name=\"app_name\">GitHub Store</string>\n\n    <string name=\"installed_apps\">Zainstalowane aplikacje</string>\n    <string name=\"navigate_back\">Cofnij</string>\n    <string name=\"check_for_updates\">Sprawdź aktualizacje</string>\n\n    <string name=\"cannot_launch\">Nie można uruchomić %1$s</string>\n    <string name=\"failed_to_open\">Nie udało się otworzyć %1$s</string>\n    <string name=\"failed_to_update\">Nie udało się zaktualizować %1$s: %2$s</string>\n    <string name=\"update_failed\">Aktualizacja nie powiodła się</string>\n    <string name=\"update_all_failed\">Aktualizacja wszystkich nie powiodła się: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">Wszystkie aplikacje zostały pomyślnie zaktualizowane</string>\n    <string name=\"no_updates_available\">Brak dostępnych aktualizacji</string>\n\n    <string name=\"search_your_apps\">Przeszukaj swoje aplikacje</string>\n    <string name=\"no_apps_found\">Nie znaleziono aplikacji</string>\n\n    <string name=\"update_all\">Aktualizuj wszystko</string>\n    <string name=\"update\">Aktualizuj</string>\n    <string name=\"open\">Otwórz</string>\n    <string name=\"cancel\">Anuluj</string>\n\n    <string name=\"checking\">Sprawdzanie…</string>\n    <string name=\"updated_successfully\">Zaktualizowano pomyślnie</string>\n    <string name=\"error_with_message\">Błąd: %1$s</string>\n\n    <string name=\"updating_x_of_y\">Aktualizowanie %1$d z %2$d</string>\n    <string name=\"currently_updating\">Obecnie: %1$s</string>\n\n\n    <string name=\"waiting_for_authorization\">Oczekiwanie na autoryzację…</string>\n    <string name=\"signed_in\">Zalogowano!</string>\n    <string name=\"redirecting_message\">Możesz już korzystać z aplikacji. Przekierowywanie…</string>\n    <string name=\"try_again\">Spróbuj ponownie</string>\n\n    <string name=\"auth_error_with_message\">Błąd: %1$s</string>\n\n    <string name=\"enter_code_on_github\">Wprowadź ten kod na GitHubie:</string>\n    <string name=\"copy_code\">Kopiuj kod</string>\n    <string name=\"open_github\">Otwórz GitHub</string>\n\n    <string name=\"unlock_full_experience\">Odblokuj pełnię\\nmożliwości</string>\n\n    <string name=\"more_requests\">Więcej zapytań</string>\n    <string name=\"more_requests_description\">\n        Zaloguj się, aby uzyskać wyższe limity API i uniknąć przerw w działaniu.\n    </string>\n\n    <string name=\"sign_in_with_github\">Zaloguj się przez GitHub</string>\n\n    <string name=\"error_cancelled\">Anulowano</string>\n    <string name=\"error_unknown\">Nieznany błąd</string>\n\n    <string name=\"language_label\">Język:</string>\n\n    <string name=\"discover_repositories\">Odkrywaj repozytoria</string>\n    <string name=\"search_repositories_hint\">Szukaj repozytorium, opisu…</string>\n\n    <string name=\"filter_by_language\">Filtruj według języka</string>\n\n    <string name=\"results_found\">Znaleziono %1$d wyników</string>\n\n\n    <string name=\"sort_by\">Sortuj według</string>\n    <string name=\"close\">Zamknij</string>\n\n    <string name=\"sort_most_stars\">Najwięcej gwiazdek</string>\n    <string name=\"sort_most_forks\">Najwięcej forków</string>\n    <string name=\"sort_best_match\">Najlepsze dopasowanie</string>\n\n    <string name=\"sort_order_descending\">Malejąco</string>\n    <string name=\"sort_order_ascending\">Rosnąco</string>\n    <string name=\"sort_label\">Sortuj</string>\n\n    <string name=\"language_all\">Wszystkie języki</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">Wyszukiwanie nie powiodło się</string>\n    <string name=\"no_repositories_found\">Nie znaleziono repozytoriów</string>\n\n    <string name=\"profile_title\">Profil</string>\n\n    <string name=\"section_appearance\">WYGLĄD</string>\n    <string name=\"section_about\">O APLIKACJI</string>\n    <string name=\"section_network\">SIEĆ</string>\n\n    <string name=\"theme_color\">Kolor motywu</string>\n    <string name=\"amoled_black_theme\">Motyw AMOLED Black</string>\n    <string name=\"amoled_black_description\">Czyste czarne tło dla trybu ciemnego</string>\n    <string name=\"selected_color\">Wybrany kolor: %1$s</string>\n\n    <string name=\"version\">Wersja</string>\n    <string name=\"help_support\">Pomoc i wsparcie</string>\n\n    <string name=\"logout\">Wyloguj się</string>\n\n    <string name=\"logout_success\">Wylogowano pomyślnie, przekierowywanie...</string>\n    <string name=\"cache_cleared\">Pamięć podręczna wyczyszczona pomyślnie</string>\n\n    <string name=\"warning\">Ostrzeżenie!</string>\n    <string name=\"logout_confirmation\">Czy na pewno chcesz się wylogować?</string>\n\n    <string name=\"theme_dynamic\">Dynamiczny</string>\n    <string name=\"theme_ocean\">Ocean</string>\n    <string name=\"theme_purple\">Fioletowy</string>\n    <string name=\"theme_forest\">Las</string>\n    <string name=\"theme_slate\">Łupek</string>\n    <string name=\"theme_amber\">Bursztyn</string>\n\n    <string name=\"open_repository\">Otwórz repozytorium</string>\n    <string name=\"open_in_browser\">Otwórz w przeglądarce</string>\n    <string name=\"cancel_download\">Anuluj pobieranie</string>\n    <string name=\"show_install_options\">Pokaż opcje instalacji</string>\n    <string name=\"report_issue\">Zgłoś problem</string>\n\n    <string name=\"error_loading_details\">Błąd podczas ładowania szczegółów</string>\n    <string name=\"retry\">Ponów</string>\n    <string name=\"no_description\">Brak opisu.</string>\n    <string name=\"no_release_notes\">Brak informacji o wydaniu.</string>\n\n    <string name=\"about_this_app\">O tej aplikacji</string>\n    <string name=\"install_logs\">Logi instalacji</string>\n    <string name=\"author\">Autor</string>\n    <string name=\"whats_new\">Co nowego</string>\n\n    <string name=\"installed\">Zainstalowano</string>\n    <string name=\"update_available\">Dostępna aktualizacja</string>\n    <string name=\"not_available\">Niedostępne</string>\n    <string name=\"install_latest\">Zainstaluj najnowszą</string>\n    <string name=\"reinstall\">Zainstaluj ponownie</string>\n    <string name=\"update_app\">Aktualizuj aplikację</string>\n\n    <string name=\"downloading\">Pobieranie</string>\n    <string name=\"updating\">Aktualizowanie</string>\n    <string name=\"verifying\">Weryfikowanie</string>\n    <string name=\"installing\">Instalowanie</string>\n\n    <string name=\"open_in_obtainium\">Otwórz w Obtainium</string>\n    <string name=\"obtainium_description\">Zarządzaj aktualizacjami automatycznie</string>\n    <string name=\"inspect_with_appmanager\">Sprawdź w AppManager</string>\n    <string name=\"appmanager_description\">Sprawdź uprawnienia, trackery i bezpieczeństwo</string>\n\n    <string name=\"profile\">Profil</string>\n\n    <string name=\"forks\">Forki</string>\n    <string name=\"stars\">Gwiazdki</string>\n    <string name=\"issues\">Zgłoszenia</string>\n\n    <string name=\"by_author\">autor: %1$s</string>\n    <string name=\"installed_version\">• Zainstalowana: %1$s</string>\n    <string name=\"architecture_compatible\">Architektura zgodna</string>\n    <string name=\"update_to_version\">Aktualizuj do %1$s</string>\n\n    <string name=\"error_load_details\">Nie udało się załadować szczegółów</string>\n    <string name=\"installer_saved_downloads\">Instalator został zapisany w folderze Pobrane</string>\n\n    <string name=\"log_download_started\">Rozpoczęto pobieranie</string>\n    <string name=\"log_downloaded\">Pobrano</string>\n    <string name=\"log_update_started\">Rozpoczęto aktualizację</string>\n    <string name=\"log_installed\">Zainstalowano</string>\n    <string name=\"log_updated\">Zaktualizowano</string>\n    <string name=\"log_cancelled\">Anulowano</string>\n    <string name=\"log_install_started\">Rozpoczęto instalację</string>\n    <string name=\"log_error\">Błąd</string>\n    <string name=\"log_error_with_message\">Błąd: %1$s</string>\n\n    <string name=\"log_prepare_appmanager\">Przygotowywanie dla AppManager</string>\n    <string name=\"log_opened_appmanager\">Otwarto w AppManager</string>\n    <string name=\"log_permission_blocked\">Uprawnienie do instalacji zablokowane przez politykę urządzenia</string>\n    <string name=\"log_opened_external_installer\">Otwarto w zewnętrznym instalatorze</string>\n    <string name=\"install_permission_unavailable\">Uprawnienie do instalacji niedostępne</string>\n    <string name=\"install_permission_blocked_message\">APK został pomyślnie pobrany, ale to urządzenie nie pozwala na bezpośrednią instalację. Czy chcesz otworzyć go za pomocą zewnętrznego instalatora?</string>\n    <string name=\"open_with_external_installer\">Otwórz za pomocą zewnętrznego instalatora</string>\n    <string name=\"external_installer_description\">Użyj aplikacji innej firmy do zainstalowania APK</string>\n\n    <string name=\"error_generic\">Błąd: %1$s</string>\n    <string name=\"error_asset_not_supported\">Typ pliku .%1$s nie jest obsługiwany</string>\n    <string name=\"error_file_not_found\">Nie znaleziono pobranego pliku</string>\n\n    <string name=\"home_category_trending\">Na czasie</string>\n    <string name=\"home_category_hot_release\">Gorące wydanie</string>\n    <string name=\"home_category_most_popular\">Najpopularniejsze</string>\n\n    <string name=\"home_finding_repositories\">Wyszukiwanie repozytoriów...</string>\n    <string name=\"home_loading_more\">Ładowanie więcej...</string>\n    <string name=\"home_no_more_repositories\">Brak kolejnych repozytoriów</string>\n    <string name=\"home_retry\">Ponów</string>\n    <string name=\"home_failed_to_load_repositories\">Nie udało się załadować repozytoriów</string>\n    <string name=\"home_view_details\">Zobacz szczegóły</string>\n\n    <string name=\"updated_just_now\">zaktualizowano przed chwilą</string>\n    <string name=\"updated_hours_ago\">zaktualizowano %1$d godz. temu</string>\n    <string name=\"updated_yesterday\">zaktualizowano wczoraj</string>\n    <string name=\"updated_days_ago\">zaktualizowano %1$d dni temu</string>\n    <string name=\"updated_on_date\">zaktualizowano %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">Limit zapytań przekroczony</string>\n    <string name=\"rate_limit_used_all\">Wykorzystałeś wszystkie zapytania API (%1$d).</string>\n    <string name=\"rate_limit_used_all_free\">Wykorzystałeś wszystkie darmowe zapytania API (%1$d).</string>\n    <string name=\"rate_limit_resets_in_minutes\">Reset za %1$d min</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Zaloguj się, aby uzyskać 5000 zapytań na godzinę zamiast 60!</string>\n    <string name=\"rate_limit_sign_in\">Zaloguj się</string>\n    <string name=\"rate_limit_ok\">OK</string>\n    <string name=\"rate_limit_close\">Zamknij</string>\n\n    <string name=\"system_font\">Czcionka systemowa</string>\n    <string name=\"system_font_description\">Dopasuj czcionkę do urządzenia dla lepszej czytelności</string>\n\n    <string name=\"theme_light\">Jasny</string>\n    <string name=\"theme_dark\">Ciemny</string>\n    <string name=\"theme_system\">Systemowy</string>\n\n    <string name=\"added_to_favourites\">Repozytorium dodane do ulubionych</string>\n    <string name=\"removed_from_favourites\">Repozytorium usunięte z ulubionych</string>\n    <string name=\"add_to_favourites\">Dodaj do ulubionych</string>\n    <string name=\"remove_from_favourites\">Usuń z ulubionych</string>\n    <string name=\"favourites\">Ulubione</string>\n\n    <string name=\"added_just_now\">dodano przed chwilą</string>\n    <string name=\"added_hours_ago\">dodano %1$d godz. temu</string>\n    <string name=\"added_yesterday\">dodano wczoraj</string>\n    <string name=\"added_days_ago\">dodano %1$d dni temu</string>\n    <string name=\"added_on_date\">dodano %1$s</string>\n\n    <string name=\"starred_repositories\">Oznaczone gwiazdką repozytoria</string>\n    <string name=\"repository_starred\">Repozytorium jest oznaczone gwiazdką</string>\n    <string name=\"repository_not_starred\">Repozytorium nie jest oznaczone gwiazdką</string>\n    <string name=\"star_from_github\">Możesz oznaczyć repozytorium gwiazdką na GitHubie</string>\n    <string name=\"unstar_from_github\">Możesz usunąć gwiazdkę z repozytorium na GitHubie</string>\n\n    <string name=\"sign_in_required\">Wymagane logowanie</string>\n    <string name=\"no_starred_repos\">Brak oznaczonych repozytoriów</string>\n    <string name=\"sign_in_with_github_for_stars\">Zaloguj się przez GitHub, aby zobaczyć oznaczone repozytoria</string>\n    <string name=\"star_repos_hint\">Oznacz repozytoria z instalowalnymi wydaniami na GitHubie</string>\n    <string name=\"last_synced\">Ostatnia synchronizacja</string>\n    <string name=\"just_now\">Przed chwilą</string>\n    <string name=\"minutes_ago\">%1$d min temu</string>\n    <string name=\"hours_ago\">%1$d h temu</string>\n    <string name=\"days_ago\">%1$d d temu</string>\n    <string name=\"dismiss\">Zamknij</string>\n    <string name=\"sync_starred_failed\">Nie udało się zsynchronizować oznaczonych gwiazdką repozytoriów</string>\n\n    <string name=\"developer_profile_title\">Profil dewelopera</string>\n    <string name=\"open_developer_profile\">Otwórz profil dewelopera</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">Nie udało się załadować repozytoriów</string>\n    <string name=\"failed_to_load_profile\">Nie udało się załadować profilu</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Repozytoria</string>\n    <string name=\"followers\">Obserwujący</string>\n    <string name=\"following\">Obserwowani</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Szukaj repozytoriów…</string>\n    <string name=\"clear_search\">Wyczyść wyszukiwanie</string>\n    <string name=\"filter_all\">Wszystkie</string>\n    <string name=\"filter_with_releases\">Z wydaniami</string>\n    <string name=\"filter_installed\">Zainstalowane</string>\n    <string name=\"filter_favorites\">Ulubione</string>\n    <string name=\"sort\">Sortuj</string>\n    <string name=\"sort_recently_updated\">Ostatnio zaktualizowane</string>\n    <string name=\"sort_name\">Nazwa</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">repozytorium</string>\n    <string name=\"repositories_plural\">repozytoriów</string>\n    <string name=\"showing_x_of_y_repositories\">Wyświetlanie %1$d z %2$d repozytoriów</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">Brak repozytoriów z instalowalnymi wydaniami</string>\n    <string name=\"no_installed_repos\">Brak zainstalowanych repozytoriów</string>\n    <string name=\"no_favorite_repos\">Brak ulubionych repozytoriów</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">Zaktualizowano %1$s</string>\n    <string name=\"has_release\">Ma wydanie</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d lat temu</string>\n    <string name=\"time_months_ago\">%1$d mies temu</string>\n    <string name=\"time_days_ago\">%1$d d temu</string>\n    <string name=\"time_hours_ago\">%1$d h temu</string>\n    <string name=\"time_minutes_ago\">%1$d min temu</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n    <string name=\"released_just_now\">Wydano przed chwilą</string>\n    <string name=\"released_hours_ago\">Wydano %1$d godzin(y) temu</string>\n    <string name=\"released_yesterday\">Wydano wczoraj</string>\n    <string name=\"released_days_ago\">Wydano %1$d dzień/dni temu</string>\n    <string name=\"released_on_date\">Wydano %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">Strona główna</string>\n    <string name=\"bottom_nav_search_title\">Szukaj</string>\n    <string name=\"bottom_nav_apps_title\">Aplikacje</string>\n    <string name=\"bottom_nav_profile_title\">Profil</string>\n\n    <string name=\"forked_repository\">Fork</string>\n\n    <string name=\"category_stable\">Stabilna</string>\n    <string name=\"category_pre_release\">Wersja przedpremierowa</string>\n    <string name=\"category_all\">Wszystkie</string>\n    <string name=\"select_version\">Wybierz wersję</string>\n    <string name=\"pre_release_badge\">Przedpremierowa</string>\n    <string name=\"no_version_selected\">Nie wybrano wersji</string>\n    <string name=\"versions_title\">Wersje</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">Oczekuje na instalację</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Odinstaluj</string>\n    <string name=\"open_app\">Otwórz</string>\n    <string name=\"downgrade_requires_uninstall\">Obniżenie wersji wymaga odinstalowania</string>\n    <string name=\"downgrade_warning_message\">Instalacja wersji %1$s wymaga odinstalowania bieżącej wersji (%2$s). Dane aplikacji zostaną utracone.</string>\n    <string name=\"uninstall_first\">Najpierw odinstaluj</string>\n    <string name=\"install_version\">Zainstaluj %1$s</string>\n    <string name=\"failed_to_open_app\">Nie udało się otworzyć %1$s</string>\n    <string name=\"failed_to_uninstall\">Nie udało się odinstalować %1$s</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">Najnowsza</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Ostatnio sprawdzono: %1$s</string>\n    <string name=\"last_checked_never\">Nigdy nie sprawdzano</string>\n    <string name=\"last_checked_just_now\">właśnie teraz</string>\n    <string name=\"last_checked_minutes_ago\">%1$d min temu</string>\n    <string name=\"last_checked_hours_ago\">%1$d godz. temu</string>\n    <string name=\"checking_for_updates\">Sprawdzanie aktualizacji…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Typ proxy</string>\n    <string name=\"proxy_none\">Brak</string>\n    <string name=\"proxy_system\">Systemowy</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Host</string>\n    <string name=\"proxy_port\">Port</string>\n    <string name=\"proxy_username\">Nazwa użytkownika (opcjonalnie)</string>\n    <string name=\"proxy_password\">Hasło (opcjonalnie)</string>\n    <string name=\"proxy_save\">Zapisz Proxy</string>\n    <string name=\"proxy_saved\">Ustawienia proxy zostały zapisane</string>\n    <string name=\"proxy_system_description\">Używa ustawień proxy urządzenia</string>\n    <string name=\"proxy_port_error\">Port musi być z zakresu 1–65535</string>\n    <string name=\"proxy_none_description\">Połączenie bezpośrednie, bez proxy</string>\n    <string name=\"failed_to_save_proxy_settings\">Nie udało się zapisać ustawień proxy</string>\n    <string name=\"proxy_host_required\">Host proxy jest wymagany</string>\n    <string name=\"invalid_proxy_port\">Nieprawidłowy port proxy</string>\n    <string name=\"proxy_show_password\">Pokaż hasło</string>\n    <string name=\"proxy_hide_password\">Ukryj hasło</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Śledź tę aplikację</string>\n    <string name=\"app_tracked_successfully\">Aplikacja dodana do listy śledzonych</string>\n    <string name=\"failed_to_track_app\">Nie udało się śledzić aplikacji: %1$s</string>\n    <string name=\"already_tracked\">Aplikacja jest już śledzona</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">Zaloguj się przez GitHub</string>\n    <string name=\"profile_sign_in_description\">Odblokuj pełnię możliwości. Zarządzaj aplikacjami, synchronizuj preferencje i przeglądaj szybciej.</string>\n    <string name=\"profile_repos\">Repozytoria</string>\n    <string name=\"profile_login\">Zaloguj się</string>\n    <string name=\"profile_stars_description\">Twoje repozytoria oznaczone gwiazdką na GitHubie</string>\n    <string name=\"profile_favourites_description\">Twoje ulubione repozytoria zapisane lokalnie</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Sesja wygasła</string>\n    <string name=\"session_expired_message\">Twoja sesja GitHub wygasła lub token został unieważniony. Zaloguj się ponownie, aby kontynuować korzystanie z funkcji wymagających uwierzytelnienia.</string>\n    <string name=\"session_expired_hint\">Możesz nadal przeglądać jako gość z ograniczoną liczbą zapytań API.</string>\n    <string name=\"sign_in_again\">Zaloguj się ponownie</string>\n    <string name=\"continue_as_guest\">Kontynuuj jako gość</string>\n    <string name=\"logout_revocation_note\">Spowoduje to wyczyszczenie lokalnej sesji i danych z pamięci podręcznej. Aby całkowicie cofnąć dostęp, odwiedź GitHub Settings > Applications.</string>\n    <string name=\"auth_code_expires_in\">Kod wygasa za %1$s</string>\n    <string name=\"auth_error_code_expired\">Kod urządzenia wygasł.</string>\n    <string name=\"auth_hint_try_again\">Spróbuj zalogować się ponownie, aby uzyskać nowy kod.</string>\n    <string name=\"auth_hint_check_connection\">Sprawdź połączenie internetowe i spróbuj ponownie.</string>\n    <string name=\"auth_hint_denied\">Odrzuciłeś żądanie autoryzacji. Spróbuj ponownie, jeśli było to niezamierzone.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Czytaj więcej</string>\n    <string name=\"show_less\">Pokaż mniej</string>\n\n    <string name=\"share_repository\">Udostępnij repozytorium</string>\n    <string name=\"failed_to_share_link\">Nie udało się udostępnić linku</string>\n    <string name=\"link_copied_to_clipboard\">Link skopiowany do schowka</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Tłumacz</string>\n    <string name=\"translating\">Tłumaczenie…</string>\n    <string name=\"show_original\">Pokaż oryginał</string>\n    <string name=\"translated_to\">Przetłumaczono na %1$s</string>\n    <string name=\"translate_to\">Tłumacz na…</string>\n    <string name=\"search_language\">Szukaj języka</string>\n    <string name=\"change_language\">Zmień język</string>\n    <string name=\"translation_failed\">Tłumaczenie nie powiodło się. Spróbuj ponownie.</string>\n\n    <string name=\"open_github_link\">Otwórz link GitHub</string>\n    <string name=\"clipboard_link_detected\">Wykryto link GitHub w schowku</string>\n    <string name=\"auto_detect_clipboard_links\">Automatyczne wykrywanie linków ze schowka</string>\n    <string name=\"auto_detect_clipboard_description\">Automatycznie wykrywaj linki GitHub ze schowka przy otwieraniu wyszukiwania</string>\n    <string name=\"detected_links\">Wykryte linki</string>\n    <string name=\"open_in_app\">Otwórz w aplikacji</string>\n    <string name=\"no_github_link_in_clipboard\">Nie znaleziono linku GitHub w schowku</string>\n\n    <string name=\"storage\">Przechowywanie</string>\n    <string name=\"clear_cache\">Wyczyść pamięć podręczną</string>\n    <string name=\"current_size\">Aktualny rozmiar:</string>\n    <string name=\"clear\">Wyczyść</string>\n\n    <string name=\"sponsor_title\">Wesprzyj GitHub Store</string>\n    <string name=\"sponsor_button\">Wesprzyj projekt</string>\n    <string name=\"sponsor_hero_title\">Zbudowane z pasją,\\nutrzymywane kawą</string>\n\n    <string name=\"sponsor_hero_subtitle\">GitHub Store osiągnął ponad 130 000 pobrań i 7 700 gwiazdek na GitHub — 100% darmowy, bez reklam i śledzenia.</string>\n\n    <string name=\"sponsor_personal_note\">Tworzę i utrzymuję ten projekt samodzielnie podczas kończenia szkoły średniej.</string>\n\n    <string name=\"sponsor_kodee_title\">Głosuj na GitHub Store!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store został nominowany do Golden Kodee Awards na KotlinConf 2026.</string>\n\n    <string name=\"sponsor_kodee_register\">1. Zarejestruj się</string>\n    <string name=\"sponsor_kodee_vote\">2. Głosuj</string>\n\n    <string name=\"sponsor_kodee_deadline\">Głosowanie kończy się 22 marca</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Zarejestruj się na platformie (Kontynuuj z Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. Kliknij Głosuj poniżej</string>\n    <string name=\"sponsor_kodee_step3\">3. Znajdź Usmon Narzullayev i kliknij Głosuj</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">Wsparcie jednorazowe lub cykliczne przez GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Szybkie jednorazowe wsparcie</string>\n\n    <string name=\"sponsor_other_ways_title\">INNE SPOSOBY POMOCY</string>\n\n    <string name=\"sponsor_star_repo\">Dodaj gwiazdkę repozytorium</string>\n    <string name=\"sponsor_star_repo_desc\">Pomaga innym odkryć GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">Zgłoś błąd</string>\n    <string name=\"sponsor_report_bugs_desc\">Poprawia aplikację dla wszystkich</string>\n\n    <string name=\"sponsor_share\">Udostępnij znajomym</string>\n    <string name=\"sponsor_share_desc\">Powiedz o tym innym programistom</string>\n\n    <string name=\"sponsor_thank_you\">Każde wsparcie — finansowe lub nie — utrzymuje ten projekt przy życiu. Dziękuję!</string>\n\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Instalacja</string>\n    <string name=\"installer_type_default\">Domyślny</string>\n    <string name=\"installer_type_default_description\">Standardowe okno instalacji systemowej</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Cicha instalacja bez potwierdzeń</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku nie jest zainstalowany</string>\n    <string name=\"shizuku_status_not_running\">Shizuku nie jest uruchomiony</string>\n    <string name=\"shizuku_status_permission_needed\">Wymagane uprawnienie</string>\n    <string name=\"shizuku_status_ready\">Gotowy</string>\n    <string name=\"shizuku_grant_permission\">Przyznaj uprawnienie</string>\n    <string name=\"shizuku_install_hint\">Zainstaluj Shizuku, aby włączyć cichą instalację</string>\n    <string name=\"shizuku_start_hint\">Uruchom Shizuku, aby włączyć cichą instalację</string>\n    <string name=\"shizuku_install_failed_fallback\">Instalacja przez Shizuku nie powiodła się, używam standardowego instalatora</string>\n\n    <string name=\"auto_update_title\">Automatyczna aktualizacja</string>\n    <string name=\"auto_update_description\">Automatycznie pobieraj i instaluj aktualizacje w tle przez Shizuku</string>\n\n    <string name=\"section_updates\">Aktualizacje</string>\n    <string name=\"update_check_interval_title\">Częstotliwość sprawdzania</string>\n    <string name=\"update_check_interval_description\">Jak często sprawdzać aktualizacje aplikacji w tle</string>\n    <string name=\"interval_3h\">3g</string>\n    <string name=\"interval_6h\">6g</string>\n    <string name=\"interval_12h\">12g</string>\n    <string name=\"interval_24h\">24g</string>\n\n    <string name=\"add_by_link\">Dodaj przez link</string>\n    <string name=\"link_app_title\">Połącz aplikację z repozytorium</string>\n    <string name=\"pick_installed_app\">Wybierz zainstalowaną aplikację, aby połączyć ją z repozytorium GitHub</string>\n    <string name=\"search_apps_hint\">Szukaj aplikacji…</string>\n    <string name=\"enter_repo_url\">URL repozytorium GitHub</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Weryfikacja…</string>\n    <string name=\"link_and_track\">Połącz i śledź</string>\n    <string name=\"checking_release\">Sprawdzanie najnowszego wydania…</string>\n    <string name=\"downloading_for_verification\">Pobieranie APK do weryfikacji…</string>\n    <string name=\"verifying_signing_key\">Weryfikacja klucza podpisu…</string>\n    <string name=\"package_name_mismatch\">Niezgodność nazwy pakietu: APK to %1$s, ale wybrana aplikacja to %2$s</string>\n    <string name=\"signing_key_mismatch_link\">Niezgodność klucza podpisu: APK z tego repozytorium został podpisany przez innego programistę</string>\n    <string name=\"select_asset_title\">Wybierz instalator</string>\n    <string name=\"select_asset_description\">Wybierz APK do weryfikacji z zainstalowaną aplikacją</string>\n    <string name=\"download_failed\">Pobieranie nie powiodło się</string>\n    <string name=\"export_apps\">Eksportuj</string>\n    <string name=\"import_apps\">Importuj</string>\n    <string name=\"import_apps_title\">Importuj aplikacje</string>\n    <string name=\"import_apps_description\">Wklej wyeksportowany JSON, aby przywrócić śledzone aplikacje</string>\n    <string name=\"import_apps_hint\">Wklej wyeksportowany JSON tutaj…</string>\n    <string name=\"include_pre_releases_title\">Uwzględnij wersje wstępne</string>\n    <string name=\"include_pre_releases_description\">Śledź wersje wstępne podczas sprawdzania aktualizacji. Po wyłączeniu uwzględniane są tylko stabilne wydania.</string>\n    <string name=\"confirm_uninstall_title\">Odinstalować aplikację?</string>\n    <string name=\"confirm_uninstall_message\">Czy na pewno chcesz odinstalować %1$s? Tej czynności nie można cofnąć, a dane aplikacji mogą zostać utracone.</string>\n    <string name=\"invalid_github_url\">Nieprawidłowy URL GitHub. Użyj formatu: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Repozytorium nie znaleziono: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">Przekroczono limit zapytań GitHub API. Spróbuj później.</string>\n    <string name=\"failed_to_link\">Nie udało się połączyć: %1$s</string>\n    <string name=\"failed_to_load_apps\">Nie udało się załadować zainstalowanych aplikacji</string>\n    <string name=\"app_linked_success\">%1$s połączono z %2$s/%3$s</string>\n    <string name=\"export_failed\">Eksport nie powiódł się: %1$s</string>\n    <string name=\"import_failed\">Import nie powiódł się: %1$s</string>\n    <string name=\"imported_apps_summary\">Zaimportowano %1$d aplikacji</string>\n    <string name=\"imported_skipped\">, %1$d pominięto</string>\n    <string name=\"imported_failed\">, %1$d nie powiodło się</string>\n    <string name=\"signing_key_changed_title\">Klucz podpisu zmieniony</string>\n    <string name=\"signing_key_changed_message\">Certyfikat podpisu tej aplikacji zmienił się od pierwszej instalacji.\\n\\nMoże to oznaczać, że programista zmienił klucz podpisu lub plik binarny mógł zostać zmodyfikowany.\\n\\nOczekiwano: %1$s\\nOtrzymano: %2$s</string>\n    <string name=\"install_anyway\">Zainstaluj mimo to</string>\n    <string name=\"verified_build\">Zweryfikowana kompilacja</string>\n    <string name=\"checking_attestation\">Sprawdzanie\\u2026</string>\n    <string name=\"assets_title\">Zasoby</string>\n    <string name=\"no_assets_selected\">Brak zasobów</string>\n    <string name=\"no_assets_in_list\">Brak zasobów powiązanych z tym wydaniem</string>\n    <string name=\"assets_selection_label\">Wybierz opcję zasobu</string>\n    <string name=\"multiple_assets_info_dialog_title\">Dostępnych wiele zasobów</string>\n    <string name=\"multiple_assets_info_dialog_text\">Dla tego wydania dostępnych jest wiele plików do zainstalowania. Przejrzyj listę i wybierz odpowiedni dla swojego urządzenia.</string>\n    <string name=\"icon_content_description_info\">Informacje</string>\n    <string name=\"translation_error_retry\">Ponów</string>\n    <string name=\"translated_from\">Wykryto automatycznie: %1$s</string>\n    <string name=\"select_language\">Wybierz język</string>\n    <string name=\"update_package_mismatch\">Niezgodność pakietu: APK to %1$s, ale zainstalowana aplikacja to %2$s. Aktualizacja zablokowana.</string>\n    <string name=\"update_signing_key_mismatch\">Niezgodność klucza podpisu: aktualizacja została podpisana przez innego programistę. Aktualizacja zablokowana.</string>\n    <string name=\"liquid_glass_option_title\">Efekt płynnego szkła</string>\n    <string name=\"liquid_glass_option_description\">Ulepsz interfejs o gładki, szklany wygląd</string>\n\n    <string name=\"hide_seen_title\">Ukryj przeglądane repozytoria</string>\n    <string name=\"hide_seen_description\">Ukryj repozytoria, które już przeglądałeś, z kanałów odkrywania</string>\n    <string name=\"clear_seen_history\">Wyczyść historię przeglądania</string>\n    <string name=\"clear_seen_history_description\">Zresetuj wszystkie przeglądane repozytoria, aby ponownie pojawiły się w kanałach</string>\n    <string name=\"seen_history_cleared\">Historia przeglądania wyczyszczona</string>\n    <string name=\"seen_badge\">Przeglądane</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml",
    "content": "<resources>\n    <string name=\"app_name\">GitHub Store</string>\n\n    <string name=\"installed_apps\">Установленные приложения</string>\n    <string name=\"navigate_back\">Назад</string>\n    <string name=\"check_for_updates\">Проверить обновления</string>\n\n    <string name=\"cannot_launch\">Не удалось запустить %1$s</string>\n    <string name=\"failed_to_open\">Не удалось открыть %1$s</string>\n    <string name=\"failed_to_update\">Не удалось обновить %1$s: %2$s</string>\n    <string name=\"update_failed\">Ошибка обновления</string>\n    <string name=\"update_all_failed\">Ошибка обновления всех: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">Все приложения успешно обновлены</string>\n    <string name=\"no_updates_available\">Обновлений нет</string>\n\n    <string name=\"search_your_apps\">Поиск приложений</string>\n    <string name=\"no_apps_found\">Приложения не найдены</string>\n\n    <string name=\"update_all\">Обновить все</string>\n    <string name=\"update\">Обновить</string>\n    <string name=\"open\">Открыть</string>\n    <string name=\"cancel\">Отмена</string>\n\n    <string name=\"checking\">Проверка…</string>\n    <string name=\"updated_successfully\">Успешно обновлено</string>\n    <string name=\"error_with_message\">Ошибка: %1$s</string>\n\n    <string name=\"updating_x_of_y\">Обновление %1$d из %2$d</string>\n    <string name=\"currently_updating\">Сейчас: %1$s</string>\n\n    <string name=\"percent\">%1$d%%</string>\n\n    <string name=\"waiting_for_authorization\">Ожидание авторизации…</string>\n    <string name=\"signed_in\">Вход выполнен!</string>\n    <string name=\"redirecting_message\">Теперь вы можете использовать приложение. Перенаправление…</string>\n    <string name=\"try_again\">Попробовать снова</string>\n\n    <string name=\"auth_error_with_message\">Ошибка: %1$s</string>\n\n    <string name=\"enter_code_on_github\">Введите этот код на GitHub:</string>\n    <string name=\"copy_code\">Скопировать код</string>\n    <string name=\"open_github\">Открыть GitHub</string>\n\n    <string name=\"unlock_full_experience\">Откройте полный\\nдоступ</string>\n\n    <string name=\"more_requests\">Больше запросов</string>\n    <string name=\"more_requests_description\">\n        Войдите, чтобы получить более высокий лимит API и избежать прерываний.\n    </string>\n\n    <string name=\"sign_in_with_github\">Войти через GitHub</string>\n\n    <string name=\"error_cancelled\">Отменено</string>\n    <string name=\"error_unknown\">Неизвестная ошибка</string>\n\n    <string name=\"language_label\">Язык:</string>\n\n    <string name=\"discover_repositories\">Поиск репозиториев</string>\n    <string name=\"search_repositories_hint\">Поиск по репозиторию, описанию…</string>\n\n    <string name=\"filter_by_language\">Фильтр по языку</string>\n\n    <string name=\"results_found\">Найдено результатов: %1$d</string>\n\n    <string name=\"sort_by\">Сортировать по</string>\n    <string name=\"close\">Закрыть</string>\n\n    <string name=\"sort_most_stars\">Больше звёзд</string>\n    <string name=\"sort_most_forks\">Больше форков</string>\n    <string name=\"sort_best_match\">Лучшее совпадение</string>\n\n    <string name=\"sort_order_descending\">По убыванию</string>\n    <string name=\"sort_order_ascending\">По возрастанию</string>\n    <string name=\"sort_label\">Сортировать</string>\n\n    <string name=\"language_all\">Все языки</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">Поиск не удался</string>\n    <string name=\"no_repositories_found\">Репозитории не найдены</string>\n\n    <string name=\"profile_title\">Профиль</string>\n\n    <string name=\"section_appearance\">ВНЕШНИЙ ВИД</string>\n    <string name=\"section_about\">О ПРИЛОЖЕНИИ</string>\n    <string name=\"section_network\">СЕТЬ</string>\n\n    <string name=\"theme_color\">Цвет темы</string>\n    <string name=\"amoled_black_theme\">AMOLED чёрная тема</string>\n    <string name=\"amoled_black_description\">Чёрный фон для тёмного режима</string>\n    <string name=\"selected_color\">Выбранный цвет: %1$s</string>\n\n    <string name=\"version\">Версия</string>\n    <string name=\"help_support\">Помощь и поддержка</string>\n\n    <string name=\"logout\">Выйти</string>\n\n    <string name=\"logout_success\">Вы успешно вышли, перенаправление...</string>\n    <string name=\"cache_cleared\">Кэш успешно очищен</string>\n\n    <string name=\"warning\">Внимание!</string>\n    <string name=\"logout_confirmation\">Вы уверены, что хотите выйти?</string>\n\n    <string name=\"theme_dynamic\">Динамическая</string>\n    <string name=\"theme_ocean\">Океан</string>\n    <string name=\"theme_purple\">Фиолетовая</string>\n    <string name=\"theme_forest\">Лесная</string>\n    <string name=\"theme_slate\">Сланцевая</string>\n    <string name=\"theme_amber\">Янтарная</string>\n\n    <string name=\"open_repository\">Открыть репозиторий</string>\n    <string name=\"cancel_download\">Отменить загрузку</string>\n\n    <string name=\"error_loading_details\">Ошибка загрузки данных</string>\n    <string name=\"retry\">Повторить</string>\n    <string name=\"no_description\">Описание отсутствует.</string>\n\n    <string name=\"about_this_app\">Об этом приложении</string>\n    <string name=\"install_logs\">Журнал установки</string>\n    <string name=\"author\">Автор</string>\n    <string name=\"whats_new\">Что нового</string>\n\n    <string name=\"installed\">Установлено</string>\n    <string name=\"update_available\">Доступно обновление</string>\n    <string name=\"not_available\">Недоступно</string>\n    <string name=\"install_latest\">Установить последнюю</string>\n    <string name=\"reinstall\">Переустановить</string>\n    <string name=\"update_app\">Обновить приложение</string>\n    <string name=\"report_issue\">Сообщить о проблеме</string>\n\n    <string name=\"downloading\">Загрузка</string>\n    <string name=\"updating\">Обновление</string>\n    <string name=\"verifying\">Проверка</string>\n    <string name=\"installing\">Установка</string>\n\n    <string name=\"open_in_obtainium\">Открыть в Obtainium</string>\n    <string name=\"obtainium_description\">Автоматическое управление обновлениями</string>\n    <string name=\"inspect_with_appmanager\">Проверить в AppManager</string>\n    <string name=\"appmanager_description\">Разрешения, трекеры и безопасность</string>\n\n    <string name=\"profile\">Профиль</string>\n\n    <string name=\"forks\">Форки</string>\n    <string name=\"stars\">Звёзды</string>\n    <string name=\"issues\">Проблемы</string>\n\n    <string name=\"by_author\">от %1$s</string>\n    <string name=\"installed_version\">• Установлено: %1$s</string>\n    <string name=\"architecture_compatible\">Совместимо с архитектурой</string>\n    <string name=\"update_to_version\">Обновить до %1$s</string>\n\n    <string name=\"no_release_notes\">Нет заметок о выпуске</string>\n    <string name=\"open_in_browser\">Открыть в браузере</string>\n    <string name=\"show_install_options\">Показать параметры установки</string>\n\n    <string name=\"error_load_details\">Не удалось загрузить данные</string>\n    <string name=\"installer_saved_downloads\">Установщик сохранён в папку Загрузки</string>\n\n    <string name=\"log_download_started\">Загрузка начата</string>\n    <string name=\"log_downloaded\">Загружено</string>\n    <string name=\"log_update_started\">Обновление начато</string>\n    <string name=\"log_installed\">Установлено</string>\n    <string name=\"log_updated\">Обновлено</string>\n    <string name=\"log_cancelled\">Отменено</string>\n    <string name=\"log_install_started\">Установка началась</string>\n    <string name=\"log_error\">Ошибка</string>\n    <string name=\"log_error_with_message\">Ошибка: %1$s</string>\n\n\n    <string name=\"log_prepare_appmanager\">Подготовка для AppManager</string>\n    <string name=\"log_opened_appmanager\">Открыто в AppManager</string>\n    <string name=\"log_permission_blocked\">Разрешение на установку заблокировано политикой устройства</string>\n    <string name=\"log_opened_external_installer\">Открыто во внешнем установщике</string>\n    <string name=\"install_permission_unavailable\">Разрешение на установку недоступно</string>\n    <string name=\"install_permission_blocked_message\">APK был успешно загружен, но это устройство не разрешает прямую установку. Хотите открыть его с помощью внешнего установщика?</string>\n    <string name=\"open_with_external_installer\">Открыть во внешнем установщике</string>\n    <string name=\"external_installer_description\">Использовать стороннее приложение для установки APK</string>\n\n    <string name=\"error_generic\">Ошибка: %1$s</string>\n    <string name=\"error_asset_not_supported\">Тип файла .%1$s не поддерживается</string>\n    <string name=\"error_file_not_found\">Загруженный файл не найден</string>\n\n    <string name=\"home_category_trending\">В тренде</string>\n    <string name=\"home_category_hot_release\">Горячий релиз</string>\n    <string name=\"home_category_most_popular\">Самые популярные</string>\n\n    <string name=\"home_finding_repositories\">Поиск репозиториев...</string>\n    <string name=\"home_loading_more\">Загрузка...</string>\n    <string name=\"home_no_more_repositories\">Больше репозиториев нет</string>\n    <string name=\"home_retry\">Повторить</string>\n    <string name=\"home_failed_to_load_repositories\">Не удалось загрузить репозитории</string>\n    <string name=\"home_view_details\">Подробнее</string>\n\n    <string name=\"updated_just_now\">обновлено только что</string>\n    <string name=\"updated_hours_ago\">обновлено %1$d ч. назад</string>\n    <string name=\"updated_yesterday\">обновлено вчера</string>\n    <string name=\"updated_days_ago\">обновлено %1$d дн. назад</string>\n    <string name=\"updated_on_date\">обновлено %1$s</string>\n\n    <string name=\"rate_limit_exceeded\">Превышен лимит запросов</string>\n    <string name=\"rate_limit_used_all\">Вы использовали все %1$d API-запросов.</string>\n    <string name=\"rate_limit_used_all_free\">Вы использовали все %1$d бесплатных API-запросов.</string>\n    <string name=\"rate_limit_resets_in_minutes\">Сброс через %1$d мин</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Войдите, чтобы получить 5 000 запросов в час вместо 60!</string>\n    <string name=\"rate_limit_sign_in\">Войти</string>\n    <string name=\"rate_limit_ok\">ОК</string>\n    <string name=\"rate_limit_close\">Закрыть</string>\n\n    <string name=\"system_font\">Системный шрифт</string>\n    <string name=\"system_font_description\">Используйте шрифт вашего устройства для лучшей читаемости</string>\n\n    <string name=\"theme_light\">Светлая</string>\n    <string name=\"theme_dark\">Тёмная</string>\n    <string name=\"theme_system\">Системная</string>\n\n    <string name=\"added_to_favourites\">Репозиторий добавлен в избранное</string>\n    <string name=\"removed_from_favourites\">Репозиторий удалён из избранного</string>\n    <string name=\"add_to_favourites\">Добавить в избранное</string>\n    <string name=\"remove_from_favourites\">Удалить из избранного</string>\n    <string name=\"favourites\">Избранное</string>\n\n    <string name=\"added_just_now\">только что добавлено</string>\n    <string name=\"added_hours_ago\">добавлено %1$d час(ов) назад</string>\n    <string name=\"added_yesterday\">добавлено вчера</string>\n    <string name=\"added_days_ago\">добавлено %1$d дн. назад</string>\n    <string name=\"added_on_date\">добавлено %1$s</string>\n\n    <string name=\"starred_repositories\">Избранные репозитории</string>\n    <string name=\"repository_starred\">Репозиторий добавлен в избранное</string>\n    <string name=\"repository_not_starred\">Репозиторий не в избранном</string>\n    <string name=\"star_from_github\">Вы можете добавить репозиторий в избранное на GitHub</string>\n    <string name=\"unstar_from_github\">Вы можете убрать репозиторий из избранного на GitHub</string>\n\n    <string name=\"sign_in_required\">Требуется вход</string>\n    <string name=\"no_starred_repos\">Нет избранных репозиториев</string>\n    <string name=\"sign_in_with_github_for_stars\">Войдите через GitHub, чтобы увидеть избранные репозитории</string>\n    <string name=\"star_repos_hint\">Отмечайте репозитории с установочными релизами на GitHub</string>\n    <string name=\"last_synced\">Последняя синхронизация</string>\n    <string name=\"just_now\">Только что</string>\n    <string name=\"minutes_ago\">%1$d мин назад</string>\n    <string name=\"hours_ago\">%1$d ч назад</string>\n    <string name=\"days_ago\">%1$d д назад</string>\n    <string name=\"dismiss\">Закрыть</string>\n    <string name=\"sync_starred_failed\">Не удалось синхронизировать избранные репозитории</string>\n\n    <string name=\"developer_profile_title\">Профиль разработчика</string>\n    <string name=\"open_developer_profile\">Открытый профиль разработчика</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">Не удалось загрузить репозитории</string>\n    <string name=\"failed_to_load_profile\">Не удалось загрузить профиль</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Репозитории</string>\n    <string name=\"followers\">Подписчики</string>\n    <string name=\"following\">Подписки</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Поиск репозиториев…</string>\n    <string name=\"clear_search\">Очистить поиск</string>\n    <string name=\"filter_all\">Все</string>\n    <string name=\"filter_with_releases\">С релизами</string>\n    <string name=\"filter_installed\">Установленные</string>\n    <string name=\"filter_favorites\">Избранные</string>\n    <string name=\"sort\">Сортировка</string>\n    <string name=\"sort_recently_updated\">Недавно обновлённые</string>\n    <string name=\"sort_name\">Название</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">репозиторий</string>\n    <string name=\"repositories_plural\">репозиториев</string>\n    <string name=\"showing_x_of_y_repositories\">Показано %1$d из %2$d репозиториев</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">Нет репозиториев с устанавливаемыми релизами</string>\n    <string name=\"no_installed_repos\">Нет установленных репозиториев</string>\n    <string name=\"no_favorite_repos\">Нет избранных репозиториев</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">Обновлено %1$s</string>\n    <string name=\"has_release\">Есть релиз</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d г</string>\n    <string name=\"time_months_ago\">%1$d мес</string>\n    <string name=\"time_days_ago\">%1$d д</string>\n    <string name=\"time_hours_ago\">%1$d ч</string>\n    <string name=\"time_minutes_ago\">%1$d мин</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">Опубликовано только что</string>\n    <string name=\"released_hours_ago\">Опубликовано %1$d час(ов) назад</string>\n    <string name=\"released_yesterday\">Опубликовано вчера</string>\n    <string name=\"released_days_ago\">Опубликовано %1$d день(дней) назад</string>\n    <string name=\"released_on_date\">Опубликовано %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">Главная</string>\n    <string name=\"bottom_nav_search_title\">Поиск</string>\n    <string name=\"bottom_nav_apps_title\">Приложения</string>\n    <string name=\"bottom_nav_profile_title\">Профиль</string>\n\n    <string name=\"forked_repository\">Форк</string>\n\n    <string name=\"category_stable\">Стабильная</string>\n    <string name=\"category_pre_release\">Предварительный релиз</string>\n    <string name=\"category_all\">Все</string>\n    <string name=\"select_version\">Выбрать версию</string>\n    <string name=\"pre_release_badge\">Предрелиз</string>\n    <string name=\"no_version_selected\">Версия не выбрана</string>\n    <string name=\"versions_title\">Версии</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">Ожидает установки</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Удалить</string>\n    <string name=\"open_app\">Открыть</string>\n    <string name=\"downgrade_requires_uninstall\">Для понижения версии требуется удаление</string>\n    <string name=\"downgrade_warning_message\">Для установки версии %1$s необходимо сначала удалить текущую версию (%2$s). Данные приложения будут потеряны.</string>\n    <string name=\"uninstall_first\">Сначала удалить</string>\n    <string name=\"install_version\">Установить %1$s</string>\n    <string name=\"failed_to_open_app\">Не удалось открыть %1$s</string>\n    <string name=\"failed_to_uninstall\">Не удалось удалить %1$s</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">Последняя</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Последняя проверка: %1$s</string>\n    <string name=\"last_checked_never\">Не проверялось</string>\n    <string name=\"last_checked_just_now\">только что</string>\n    <string name=\"last_checked_minutes_ago\">%1$d мин назад</string>\n    <string name=\"last_checked_hours_ago\">%1$d ч назад</string>\n    <string name=\"checking_for_updates\">Проверка обновлений…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Тип прокси</string>\n    <string name=\"proxy_none\">Нет</string>\n    <string name=\"proxy_system\">Системный</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Хост</string>\n    <string name=\"proxy_port\">Порт</string>\n    <string name=\"proxy_username\">Имя пользователя (необязательно)</string>\n    <string name=\"proxy_password\">Пароль (необязательно)</string>\n    <string name=\"proxy_save\">Сохранить прокси</string>\n    <string name=\"proxy_saved\">Настройки прокси сохранены</string>\n    <string name=\"proxy_system_description\">Использует прокси-настройки устройства</string>\n    <string name=\"proxy_port_error\">Порт должен быть 1–65535</string>\n    <string name=\"proxy_none_description\">Прямое подключение, без прокси</string>\n    <string name=\"failed_to_save_proxy_settings\">Не удалось сохранить настройки прокси</string>\n    <string name=\"proxy_host_required\">Требуется хост прокси</string>\n    <string name=\"invalid_proxy_port\">Недопустимый порт прокси</string>\n    <string name=\"proxy_show_password\">Показать пароль</string>\n    <string name=\"proxy_hide_password\">Скрыть пароль</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Отслеживать приложение</string>\n    <string name=\"app_tracked_successfully\">Приложение добавлено в список отслеживания</string>\n    <string name=\"failed_to_track_app\">Не удалось отследить приложение: %1$s</string>\n    <string name=\"already_tracked\">Приложение уже отслеживается</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">Войти через GitHub</string>\n    <string name=\"profile_sign_in_description\">Откройте полный доступ. Управляйте приложениями, синхронизируйте настройки и просматривайте быстрее.</string>\n    <string name=\"profile_repos\">Репозитории</string>\n    <string name=\"profile_login\">Войти</string>\n    <string name=\"profile_stars_description\">Ваши избранные репозитории на GitHub</string>\n    <string name=\"profile_favourites_description\">Ваши избранные репозитории, сохранённые локально</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Сессия истекла</string>\n    <string name=\"session_expired_message\">Ваша сессия GitHub истекла или токен был отозван. Пожалуйста, войдите снова для продолжения использования авторизованных функций.</string>\n    <string name=\"session_expired_hint\">Вы можете продолжить просмотр как гость с ограниченным количеством API-запросов.</string>\n    <string name=\"sign_in_again\">Войти снова</string>\n    <string name=\"continue_as_guest\">Продолжить как гость</string>\n    <string name=\"logout_revocation_note\">Это очистит вашу локальную сессию и кэшированные данные. Чтобы полностью отозвать доступ, перейдите в GitHub Settings > Applications.</string>\n    <string name=\"auth_code_expires_in\">Код истекает через %1$s</string>\n    <string name=\"auth_error_code_expired\">Срок действия кода устройства истёк.</string>\n    <string name=\"auth_hint_try_again\">Пожалуйста, попробуйте войти снова для получения нового кода.</string>\n    <string name=\"auth_hint_check_connection\">Проверьте подключение к интернету и попробуйте снова.</string>\n    <string name=\"auth_hint_denied\">Вы отклонили запрос авторизации. Попробуйте снова, если это было непреднамеренно.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Читать далее</string>\n    <string name=\"show_less\">Свернуть</string>\n\n    <string name=\"share_repository\">Поделиться репозиторием</string>\n    <string name=\"failed_to_share_link\">Не удалось поделиться ссылкой</string>\n    <string name=\"link_copied_to_clipboard\">Ссылка скопирована в буфер обмена</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Перевести</string>\n    <string name=\"translating\">Перевод…</string>\n    <string name=\"show_original\">Показать оригинал</string>\n    <string name=\"translated_to\">Переведено на %1$s</string>\n    <string name=\"translate_to\">Перевести на…</string>\n    <string name=\"search_language\">Поиск языка</string>\n    <string name=\"change_language\">Изменить язык</string>\n    <string name=\"translation_failed\">Ошибка перевода. Попробуйте ещё раз.</string>\n\n    <string name=\"open_github_link\">Открыть ссылку GitHub</string>\n    <string name=\"clipboard_link_detected\">Обнаружена ссылка GitHub в буфере обмена</string>\n    <string name=\"auto_detect_clipboard_links\">Автоопределение ссылок из буфера</string>\n    <string name=\"auto_detect_clipboard_description\">Автоматически определять ссылки GitHub из буфера обмена при открытии поиска</string>\n    <string name=\"detected_links\">Обнаруженные ссылки</string>\n    <string name=\"open_in_app\">Открыть в приложении</string>\n    <string name=\"no_github_link_in_clipboard\">Ссылка GitHub не найдена в буфере обмена</string>\n\n    <string name=\"storage\">Хранение</string>\n    <string name=\"clear_cache\">Очистить кэш</string>\n    <string name=\"current_size\">Текущий размер:</string>\n    <string name=\"clear\">Очистить</string>\n\n    <string name=\"sponsor_title\">Поддержать GitHub Store</string>\n    <string name=\"sponsor_button\">Поддержать проект</string>\n    <string name=\"sponsor_hero_title\">Создано с любовью,\\nподдерживается кофе</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store достиг более 130 000 загрузок и 7 700 звёзд на GitHub — 100% бесплатно, без рекламы и отслеживания.</string>\n\n    <string name=\"sponsor_personal_note\">Я разработал и поддерживаю этот проект полностью самостоятельно, заканчивая школу. Ваша поддержка — даже небольшая — помогает оплачивать инфраструктуру и развивать приложение.</string>\n\n    <string name=\"sponsor_kodee_title\">Голосуйте за GitHub Store!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store номинирован на Golden Kodee Awards на KotlinConf 2026.</string>\n\n    <string name=\"sponsor_kodee_register\">1. Зарегистрироваться</string>\n    <string name=\"sponsor_kodee_vote\">2. Проголосовать</string>\n    <string name=\"sponsor_kodee_deadline\">Голосование до 22 марта</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Зарегистрируйтесь на платформе (Войти через Google)</string>\n    <string name=\"sponsor_kodee_step2\">2. Нажмите «Голосовать» ниже</string>\n    <string name=\"sponsor_kodee_step3\">3. Найдите Usmon Narzullayev и нажмите «Голосовать»</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">Регулярная или разовая поддержка через GitHub</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Быстрая разовая поддержка</string>\n\n    <string name=\"sponsor_other_ways_title\">ДРУГИЕ СПОСОБЫ ПОМОЧЬ</string>\n\n    <string name=\"sponsor_star_repo\">Поставить звезду репозиторию</string>\n    <string name=\"sponsor_star_repo_desc\">Помогает другим найти GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">Сообщить об ошибке</string>\n    <string name=\"sponsor_report_bugs_desc\">Делает приложение лучше</string>\n\n    <string name=\"sponsor_share\">Поделиться с друзьями</string>\n    <string name=\"sponsor_share_desc\">Расскажите другим разработчикам</string>\n\n    <string name=\"sponsor_thank_you\">Любая поддержка — финансовая или нет — помогает проекту жить. Спасибо!</string>\n\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Установка</string>\n    <string name=\"installer_type_default\">По умолчанию</string>\n    <string name=\"installer_type_default_description\">Стандартный системный диалог установки</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Тихая установка без подтверждений</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku не установлен</string>\n    <string name=\"shizuku_status_not_running\">Shizuku не запущен</string>\n    <string name=\"shizuku_status_permission_needed\">Требуется разрешение</string>\n    <string name=\"shizuku_status_ready\">Готов</string>\n    <string name=\"shizuku_grant_permission\">Предоставить разрешение</string>\n    <string name=\"shizuku_install_hint\">Установите Shizuku для тихой установки</string>\n    <string name=\"shizuku_start_hint\">Запустите Shizuku для тихой установки</string>\n    <string name=\"shizuku_install_failed_fallback\">Установка через Shizuku не удалась, используется стандартный установщик</string>\n\n    <string name=\"auto_update_title\">Автообновление приложений</string>\n    <string name=\"auto_update_description\">Автоматически загружать и устанавливать обновления в фоне через Shizuku</string>\n\n    <string name=\"section_updates\">Обновления</string>\n    <string name=\"update_check_interval_title\">Интервал проверки обновлений</string>\n    <string name=\"update_check_interval_description\">Как часто проверять обновления приложения в фоне</string>\n    <string name=\"interval_3h\">3ч</string>\n    <string name=\"interval_6h\">6ч</string>\n    <string name=\"interval_12h\">12ч</string>\n    <string name=\"interval_24h\">24ч</string>\n\n    <string name=\"add_by_link\">Добавить по ссылке</string>\n    <string name=\"link_app_title\">Привязать приложение к репозиторию</string>\n    <string name=\"pick_installed_app\">Выберите установленное приложение для привязки к репозиторию GitHub</string>\n    <string name=\"search_apps_hint\">Поиск приложений…</string>\n    <string name=\"enter_repo_url\">URL репозитория GitHub</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Проверка…</string>\n    <string name=\"link_and_track\">Привязать и отслеживать</string>\n    <string name=\"checking_release\">Проверка последнего релиза…</string>\n    <string name=\"downloading_for_verification\">Загрузка APK для проверки…</string>\n    <string name=\"verifying_signing_key\">Проверка ключа подписи…</string>\n    <string name=\"package_name_mismatch\">Несоответствие имени пакета: APK — %1$s, а выбранное приложение — %2$s</string>\n    <string name=\"signing_key_mismatch_link\">Несоответствие ключа подписи: APK в этом репозитории подписан другим разработчиком</string>\n    <string name=\"select_asset_title\">Выберите установщик</string>\n    <string name=\"select_asset_description\">Выберите APK для проверки соответствия установленному приложению</string>\n    <string name=\"download_failed\">Ошибка загрузки</string>\n    <string name=\"export_apps\">Экспорт</string>\n    <string name=\"import_apps\">Импорт</string>\n    <string name=\"import_apps_title\">Импорт приложений</string>\n    <string name=\"import_apps_description\">Вставьте экспортированный JSON для восстановления отслеживаемых приложений</string>\n    <string name=\"import_apps_hint\">Вставьте экспортированный JSON…</string>\n    <string name=\"include_pre_releases_title\">Включить пре-релизы</string>\n    <string name=\"include_pre_releases_description\">Отслеживать пре-релизные версии при проверке обновлений. При отключении учитываются только стабильные релизы.</string>\n    <string name=\"confirm_uninstall_title\">Удалить приложение?</string>\n    <string name=\"confirm_uninstall_message\">Вы уверены, что хотите удалить %1$s? Это действие нельзя отменить, данные приложения могут быть утеряны.</string>\n    <string name=\"invalid_github_url\">Неверный URL GitHub. Используйте формат: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Репозиторий не найден: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">Превышен лимит запросов GitHub API. Попробуйте позже.</string>\n    <string name=\"failed_to_link\">Не удалось привязать: %1$s</string>\n    <string name=\"failed_to_load_apps\">Не удалось загрузить установленные приложения</string>\n    <string name=\"app_linked_success\">%1$s привязано к %2$s/%3$s</string>\n    <string name=\"export_failed\">Ошибка экспорта: %1$s</string>\n    <string name=\"import_failed\">Ошибка импорта: %1$s</string>\n    <string name=\"imported_apps_summary\">Импортировано %1$d приложений</string>\n    <string name=\"imported_skipped\">, %1$d пропущено</string>\n    <string name=\"imported_failed\">, %1$d с ошибкой</string>\n    <string name=\"signing_key_changed_title\">Ключ подписи изменён</string>\n    <string name=\"signing_key_changed_message\">Сертификат подписи этого приложения изменился с момента первой установки.\\n\\nЭто может означать, что разработчик сменил ключ подписи, или бинарный файл был изменён.\\n\\nОжидалось: %1$s\\nПолучено: %2$s</string>\n    <string name=\"install_anyway\">Всё равно установить</string>\n    <string name=\"verified_build\">Проверенная сборка</string>\n    <string name=\"checking_attestation\">Проверка\\u2026</string>\n    <string name=\"assets_title\">Ресурсы</string>\n    <string name=\"no_assets_selected\">Нет ресурсов</string>\n    <string name=\"no_assets_in_list\">Нет ресурсов, связанных с этим релизом</string>\n    <string name=\"assets_selection_label\">Выбрать вариант ресурса</string>\n    <string name=\"multiple_assets_info_dialog_title\">Доступно несколько ресурсов</string>\n    <string name=\"multiple_assets_info_dialog_text\">Для этого релиза доступно несколько устанавливаемых файлов. Просмотрите список и выберите подходящий для вашего устройства.</string>\n    <string name=\"icon_content_description_info\">Информация</string>\n    <string name=\"translation_error_retry\">Повторить</string>\n    <string name=\"translated_from\">Автоопределение: %1$s</string>\n    <string name=\"select_language\">Выбрать язык</string>\n    <string name=\"update_package_mismatch\">Несоответствие пакета: APK — %1$s, но установленное приложение — %2$s. Обновление заблокировано.</string>\n    <string name=\"update_signing_key_mismatch\">Несоответствие ключа подписи: обновление подписано другим разработчиком. Обновление заблокировано.</string>\n    <string name=\"liquid_glass_option_title\">Эффект жидкого стекла</string>\n    <string name=\"liquid_glass_option_description\">Улучшите интерфейс с помощью гладкого стеклянного оформления</string>\n\n    <string name=\"hide_seen_title\">Скрыть просмотренные репозитории</string>\n    <string name=\"hide_seen_description\">Скрыть уже просмотренные репозитории из лент обнаружения</string>\n    <string name=\"clear_seen_history\">Очистить историю просмотров</string>\n    <string name=\"clear_seen_history_description\">Сбросить все просмотренные репозитории, чтобы они снова появились в лентах</string>\n    <string name=\"seen_history_cleared\">История просмотров очищена</string>\n    <string name=\"seen_badge\">Просмотрено</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml",
    "content": "<resources>\n\n    <!-- Apps feature - App -->\n    <string name=\"app_name\">GitHub Store</string>\n\n    <!-- Apps feature - Navigation / Top bar -->\n    <string name=\"installed_apps\">Yüklü Uygulamalar</string>\n    <string name=\"navigate_back\">Geri git</string>\n    <string name=\"check_for_updates\">Güncellemeleri kontrol et</string>\n\n    <!-- Apps feature - Errors / messages -->\n    <string name=\"cannot_launch\">%1$s çalıştırılamadı</string>\n    <string name=\"failed_to_open\">%1$s açılamadı</string>\n    <string name=\"failed_to_update\">%1$s: %2$s güncellenemedi</string>\n    <string name=\"update_failed\">Güncelleme başarısız</string>\n    <string name=\"update_all_failed\">Tümünü güncelleme başarısız: %1$s</string>\n    <string name=\"all_apps_updated_successfully\">Tüm uygulamalar başarılı şekilde güncellendi</string>\n    <string name=\"no_updates_available\">Güncelleme yok</string>\n\n    <!-- Apps feature - Search -->\n    <string name=\"search_your_apps\">Uygulamalarınızı arayın</string>\n    <string name=\"no_apps_found\">Uygulama Bulunamadı</string>\n\n    <!-- Apps feature - Actions -->\n    <string name=\"update_all\">Tümünü Güncelle</string>\n    <string name=\"update\">Güncelle</string>\n    <string name=\"open\">Aç</string>\n    <string name=\"cancel\">İptal et</string>\n\n    <!-- Apps feature - Update states -->\n    <string name=\"checking\">Kontrol ediliyor…</string>\n    <string name=\"updated_successfully\">Başarıyla güncellendi</string>\n    <string name=\"error_with_message\">Hata: %1$s</string>\n\n    <!-- Apps feature - Update all progress -->\n    <string name=\"updating_x_of_y\">%2$d içinden %1$d güncelleniyor</string>\n    <string name=\"currently_updating\">Şu anda: %1$s</string>\n\n\n    <!-- Auth general -->\n    <string name=\"waiting_for_authorization\">Yetki bekleniyor</string>\n    <string name=\"signed_in\">Giriş Yapıldı!</string>\n    <string name=\"redirecting_message\">Artık uygulamayı kullanabilirsiniz. Yönlendiriliyor...</string>\n    <string name=\"try_again\">Tekrar dene</string>\n\n    <!-- Errors -->\n    <string name=\"auth_error_with_message\">Hata: %1$s</string>\n\n    <!-- Device flow -->\n    <string name=\"enter_code_on_github\">Kodu GitHub'da gir</string>\n    <string name=\"copy_code\">Kodu kopyala</string>\n    <string name=\"open_github\">Github'ı aç</string>\n\n    <!-- Logged out -->\n    <string name=\"unlock_full_experience\">Tam Deneyimi Keşfedin</string>\n\n    <string name=\"more_requests\">Daha Fazla İstek</string>\n    <string name=\"more_requests_description\">\n        Daha yüksek API hız sınırları elde etmek ve kesintileri önlemek için oturum açın. </string>\n\n    <string name=\"sign_in_with_github\">Github ile giriş yap</string>\n\n    <string name=\"error_cancelled\">İptal edildi</string>\n    <string name=\"error_unknown\">Bilinmeyen hata</string>\n\n    <string name=\"language_label\">Dil:</string>\n\n    <string name=\"discover_repositories\">Repoları keşfet</string>\n    <string name=\"search_repositories_hint\">Repo, açıklama ara...</string>\n\n    <!-- Filters -->\n    <string name=\"filter_by_language\">Dil ile filtrele</string>\n\n    <!-- Results -->\n    <string name=\"results_found\">%1$d sonuç bulundu</string>\n\n\n    <!-- Sorting -->\n    <string name=\"sort_by\">Sırala</string>\n    <string name=\"close\">Kapat</string>\n\n    <string name=\"sort_most_stars\">En Çok Yıldız</string>\n    <string name=\"sort_most_forks\">En Çok Fork</string>\n    <string name=\"sort_best_match\">En İyi Eşleşme</string>\n\n    <string name=\"sort_order_descending\">Azalan</string>\n    <string name=\"sort_order_ascending\">Artan</string>\n    <string name=\"sort_label\">Sırala</string>\n\n    <!-- Programming languages -->\n    <string name=\"language_all\">Tüm Diller</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">Arama Başarısız</string>\n    <string name=\"no_repositories_found\">Repo bulunamadı</string>\n\n    <!-- Settings -->\n    <string name=\"profile_title\">Profil</string>\n\n    <!-- Sections -->\n    <string name=\"section_appearance\">GÖRÜNÜM</string>\n    <string name=\"section_about\">HAKKINDA</string>\n    <string name=\"section_network\">AĞ</string>\n\n    <!-- Appearance -->\n    <string name=\"theme_color\">Tema Rengi</string>\n    <string name=\"amoled_black_theme\">AMOLED Siyah Tema</string>\n    <string name=\"amoled_black_description\">Karanlık mod için saf siyah arka plan</string>\n    <string name=\"selected_color\">Seçilmiş renk: %1$s</string>\n\n    <!-- About -->\n    <string name=\"version\">Sürüm</string>\n    <string name=\"help_support\">Yardım &amp; Destek</string>\n\n    <!-- Account -->\n    <string name=\"logout\">Çıkış Yap</string>\n\n    <!-- Snackbar -->\n    <string name=\"logout_success\">Başarılı şekilde çıkış yapıldı, yönlendiriliyor...</string>\n    <string name=\"cache_cleared\">Önbellek başarıyla temizlendi</string>\n\n    <!-- Dialog -->\n    <string name=\"warning\">Uyarı!</string>\n    <string name=\"logout_confirmation\">Çıkmak istediğinden emin misin?</string>\n\n    <!-- Theme names -->\n    <string name=\"theme_dynamic\">Dinamik</string>\n    <string name=\"theme_ocean\">Deniz</string>\n    <string name=\"theme_purple\">Mor</string>\n    <string name=\"theme_forest\">Orman</string>\n    <string name=\"theme_slate\">Arduvaz</string>\n    <string name=\"theme_amber\">Amber</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">Repoyu Aç</string>\n    <string name=\"open_in_browser\">Tarayıcıda aç</string>\n    <string name=\"cancel_download\">İndirmeyi iptal et</string>\n    <string name=\"show_install_options\">Yükleme seçeneklerini göster</string>\n\n    <!-- Errors & states -->\n    <string name=\"error_loading_details\">Detaylar yüklenirken hata</string>\n    <string name=\"retry\">Tekrar dene</string>\n    <string name=\"no_description\">Açıklama sağlanmadı.</string>\n    <string name=\"no_release_notes\">Sürüm notu yok</string>\n    <string name=\"report_issue\">Sorun bildir</string>\n\n    <!-- Headers -->\n    <string name=\"about_this_app\">Bu uygulama hakkında</string>\n    <string name=\"install_logs\">Yükleme günlükleri</string>\n    <string name=\"author\">Yazar</string>\n    <string name=\"whats_new\">Neler Yeni</string>\n\n    <!-- Install status -->\n    <string name=\"installed\">Yüklü</string>\n    <string name=\"update_available\">Güncelleme mevcut</string>\n    <string name=\"not_available\">Mevcut değil</string>\n    <string name=\"install_latest\">En son sürümü yükle</string>\n    <string name=\"reinstall\">Tekrar yükle</string>\n    <string name=\"update_app\">Uygulamayı güncelle</string>\n\n    <!-- Download stages -->\n    <string name=\"downloading\">İndiriliyor</string>\n    <string name=\"updating\">Güncelleniyor</string>\n    <string name=\"verifying\">Doğrulanıyor</string>\n    <string name=\"installing\">Yükleniyor</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">Obtainium'da aç</string>\n    <string name=\"obtainium_description\">Uygulamaları otomatik güncelle</string>\n    <string name=\"inspect_with_appmanager\">AppManager ile incele</string>\n    <string name=\"appmanager_description\">İzinleri, izleyici ve güvenliği kontrol et</string>\n\n    <!-- Author -->\n    <string name=\"profile\">Profil</string>\n\n    <!-- Stats -->\n    <string name=\"forks\">Forklar</string>\n    <string name=\"stars\">Yıldızlar</string>\n    <string name=\"issues\">Sorunlar</string>\n\n    <!-- Misc -->\n    <string name=\"by_author\">%1$s</string>\n    <string name=\"installed_version\">Yüklenmiş: %1$s</string>\n    <string name=\"architecture_compatible\">Mimari uyumlu</string>\n    <string name=\"update_to_version\">%1$s'e güncelle</string>\n\n    <!-- General -->\n    <string name=\"error_load_details\">Detaylar yüklenemedi</string>\n    <string name=\"installer_saved_downloads\">Yükleyici, İndirilenler klasörüne kaydedildi</string>\n\n    <!-- Download / install states -->\n    <string name=\"log_download_started\">İndirme başladı</string>\n    <string name=\"log_downloaded\">İndirildi</string>\n    <string name=\"log_update_started\">Güncelleme başladı</string>\n    <string name=\"log_installed\">Yüklendi</string>\n    <string name=\"log_updated\">Güncellendi</string>\n    <string name=\"log_cancelled\">İptal edildi</string>\n    <string name=\"log_install_started\">Yükleme Başladı</string>\n    <string name=\"log_error\">Hata</string>\n    <string name=\"log_error_with_message\">Hata: %1$s</string>\n\n    <!-- External tools -->\n    <string name=\"log_prepare_appmanager\">AppManager için hazırlanıyor</string>\n    <string name=\"log_opened_appmanager\">AppManager\\'da açıldı</string>\n    <string name=\"log_permission_blocked\">Yükleme izni cihaz politikası tarafından engellendi</string>\n    <string name=\"log_opened_external_installer\">Harici yükleyicide açıldı</string>\n    <string name=\"install_permission_unavailable\">Yükleme izni kullanılamıyor</string>\n    <string name=\"install_permission_blocked_message\">APK başarıyla indirildi ancak bu cihaz doğrudan yüklemeye izin vermiyor. Harici bir yükleyici ile açmak ister misiniz?</string>\n    <string name=\"open_with_external_installer\">Harici yükleyici ile aç</string>\n    <string name=\"external_installer_description\">APK\\'yı yüklemek için üçüncü taraf bir uygulama kullanın</string>\n\n    <!-- Errors -->\n    <string name=\"error_generic\">Hata: %1$s</string>\n    <string name=\"error_asset_not_supported\">Tür .%1$s desteklenmiyor</string>\n    <string name=\"error_file_not_found\">İndirilen dosya bulunamadı</string>\n\n    <string name=\"home_category_trending\">Trendler</string>\n    <string name=\"home_category_hot_release\">Yeni sürüm</string>\n    <string name=\"home_category_most_popular\">En popüler</string>\n\n    <string name=\"home_finding_repositories\">Repolar bulunuyor...</string>\n    <string name=\"home_loading_more\">Daha fazla yükleniyor...</string>\n    <string name=\"home_no_more_repositories\">Başka repo yok</string>\n    <string name=\"home_retry\">Tekrar dene</string>\n    <string name=\"home_failed_to_load_repositories\">Repolar yüklenemedi</string>\n    <string name=\"home_view_details\">Detayları Görüntüle</string>\n\n    <string name=\"updated_just_now\">şimdi güncellendi</string>\n    <string name=\"updated_hours_ago\">%1$d saat önce güncellendi</string>\n    <string name=\"updated_yesterday\">dün güncellendi</string>\n    <string name=\"updated_days_ago\">%1$d gün önce güncellendi</string>\n    <string name=\"updated_on_date\">%1$s tarihinde güncellendi</string>\n\n    <string name=\"rate_limit_exceeded\">Hız Sınırı Aşıldı</string>\n    <string name=\"rate_limit_used_all\">Tüm %1$d API isteklerini kullandınız.</string>\n    <string name=\"rate_limit_used_all_free\">Tüm %1$d ücretsiz API isteğinizi kullandınız.</string>\n    <string name=\"rate_limit_resets_in_minutes\">%1$d dakika içinde yenilenir</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 Saat başı 60 yerine 5.000 istek için giriş yapın!</string>\n    <string name=\"rate_limit_sign_in\">Giriş Yap</string>\n    <string name=\"rate_limit_ok\">Tamam</string>\n    <string name=\"rate_limit_close\">Kapat</string>\n\n    <string name=\"system_font\">Sistem fontu</string>\n    <string name=\"system_font_description\">Daha iyi okunabilirlik için cihazınızın yazı tipini eşleştirin</string>\n\n    <string name=\"theme_light\">Aydınlık</string>\n    <string name=\"theme_dark\">Karanlık</string>\n    <string name=\"theme_system\">Sistem</string>\n\n    <string name=\"added_to_favourites\">Repo favorilere eklendi</string>\n    <string name=\"removed_from_favourites\">Repo favorilerden kaldırıldı</string>\n    <string name=\"add_to_favourites\">Favorilere ekle</string>\n    <string name=\"remove_from_favourites\">Favorilerden kaldır</string>\n    <string name=\"favourites\">Favoriler</string>\n\n    <string name=\"added_just_now\">şimdi eklendi</string>\n    <string name=\"added_hours_ago\">%1$d saat önce eklendi</string>\n    <string name=\"added_yesterday\">dün eklendi</string>\n    <string name=\"added_days_ago\">%1$d gün önce eklendi</string>\n    <string name=\"added_on_date\">%1$s tarihinde eklendi</string>\n\n    <string name=\"starred_repositories\">Yıldızlı Repolar</string>\n    <string name=\"repository_starred\">Repo yıldızlandı</string>\n    <string name=\"repository_not_starred\">Repo yıldızlanmadı</string>\n    <string name=\"star_from_github\">GitHub'dan repo yıldızlayabilirsiniz</string>\n    <string name=\"unstar_from_github\">GitHub'dan repo yıldız kaldırabilirsiniz</string>\n\n    <string name=\"sign_in_required\">Giriş gerekli</string>\n    <string name=\"sign_in_with_github_for_stars\">GitHub ile oturum açarak yıldızlı repolarınızı görüntüleyin</string>\n    <string name=\"no_starred_repos\">Yıldızlı repo yok</string>\n    <string name=\"star_repos_hint\">Yüklenebilir sürümlü repoları görmek için GitHub'da yıldızlayın</string>\n    <string name=\"last_synced\">Son eşitleme</string>\n    <string name=\"just_now\">Şimdi</string>\n    <string name=\"minutes_ago\">%1$d dakika önce</string>\n    <string name=\"hours_ago\">%1$d saat önce</string>\n    <string name=\"days_ago\">%1$d gün önce</string>\n    <string name=\"dismiss\">Kapat</string>\n\t\t<string name=\"sync_starred_failed\">Yıldızlı repoları eşitlerken hata</string>\n\n\t\t<string name=\"developer_profile_title\">Geliştirici Profili</string>\n    <string name=\"open_developer_profile\">Geliştirici profilini aç</string>\n\n    <string name=\"failed_to_load_repositories\">Repolar yüklenirken hata</string>\n    <string name=\"failed_to_load_profile\">Profil yüklenirken hata</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">Repolar</string>\n    <string name=\"followers\">Takipçiler</string>\n    <string name=\"following\">Takip edilenler</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">Repo ara…</string>\n    <string name=\"clear_search\">Aramayı temizle</string>\n    <string name=\"filter_all\">Hepsi</string>\n    <string name=\"filter_with_releases\">Yayınlanmış</string>\n    <string name=\"filter_installed\">İndirilmiş</string>\n    <string name=\"filter_favorites\">Favoriler</string>\n    <string name=\"sort\">Sırala</string>\n    <string name=\"sort_recently_updated\">Yeni Güncellenmiş</string>\n    <string name=\"sort_name\">Ad</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">repo</string>\n    <string name=\"repositories_plural\">repolar</string>\n    <string name=\"showing_x_of_y_repositories\">%2$d repodan %1$d tanesi gösteriliyor</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">Yüklenebilir sürümleri olan repo yok</string>\n    <string name=\"no_installed_repos\">Yüklü repo yok</string>\n    <string name=\"no_favorite_repos\">Favori repo yok</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">%1$s güncellendi</string>\n    <string name=\"has_release\">Yayınlanmış</string>\n\n    <string name=\"time_years_ago\">%1$d y önce</string>\n    <string name=\"time_months_ago\">%1$d ay önce</string>\n    <string name=\"time_days_ago\">%1$d g önce</string>\n    <string name=\"time_hours_ago\">%1$d s önce</string>\n    <string name=\"time_minutes_ago\">%1$d d önce</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">Az önce yayınlandı</string>\n    <string name=\"released_hours_ago\">%1$d saat önce yayınlandı</string>\n    <string name=\"released_yesterday\">Dün yayınlandı</string>\n    <string name=\"released_days_ago\">%1$d gün önce yayınlandı</string>\n    <string name=\"released_on_date\">%1$s tarihinde yayınlandı</string>\n\n    <string name=\"bottom_nav_home_title\">Ana Sayfa</string>\n    <string name=\"bottom_nav_search_title\">Ara</string>\n    <string name=\"bottom_nav_apps_title\">Uygulamalar</string>\n    <string name=\"bottom_nav_profile_title\">Profil</string>\n\n    <string name=\"forked_repository\">Çatalla</string>\n\n    <string name=\"category_stable\">Kararlı</string>\n    <string name=\"category_pre_release\">Ön sürüm</string>\n    <string name=\"category_all\">Tümü</string>\n    <string name=\"select_version\">Sürüm seç</string>\n    <string name=\"pre_release_badge\">Ön sürüm</string>\n    <string name=\"no_version_selected\">Sürüm seçilmedi</string>\n    <string name=\"versions_title\">Sürümler</string>\n\n    <!-- Install status -->\n    <string name=\"pending_install\">Kurulum bekleniyor</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">Kaldır</string>\n    <string name=\"open_app\">Aç</string>\n    <string name=\"downgrade_requires_uninstall\">Sürüm düşürme kaldırma gerektirir</string>\n    <string name=\"downgrade_warning_message\">%1$s sürümünü yüklemek için önce mevcut sürümü (%2$s) kaldırmanız gerekir. Uygulama verileri kaybolacaktır.</string>\n    <string name=\"uninstall_first\">Önce kaldır</string>\n    <string name=\"install_version\">%1$s yükle</string>\n    <string name=\"failed_to_open_app\">%1$s açılamadı</string>\n    <string name=\"failed_to_uninstall\">%1$s kaldırılamadı</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">En son</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">Son kontrol: %1$s</string>\n    <string name=\"last_checked_never\">Hiç kontrol edilmedi</string>\n    <string name=\"last_checked_just_now\">az önce</string>\n    <string name=\"last_checked_minutes_ago\">%1$d dk önce</string>\n    <string name=\"last_checked_hours_ago\">%1$d sa önce</string>\n    <string name=\"checking_for_updates\">Güncellemeler kontrol ediliyor…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">Proxy Türü</string>\n    <string name=\"proxy_none\">Yok</string>\n    <string name=\"proxy_system\">Sistem</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">Ana Bilgisayar</string>\n    <string name=\"proxy_port\">Port</string>\n    <string name=\"proxy_username\">Kullanıcı adı (isteğe bağlı)</string>\n    <string name=\"proxy_password\">Şifre (isteğe bağlı)</string>\n    <string name=\"proxy_save\">Proxy'yi Kaydet</string>\n    <string name=\"proxy_saved\">Proxy ayarları kaydedildi</string>\n    <string name=\"proxy_system_description\">Cihazınızın proxy ayarlarını kullanır</string>\n    <string name=\"proxy_port_error\">Port 1–65535 arası olmalı</string>\n    <string name=\"proxy_none_description\">Doğrudan bağlantı, proxy yok</string>\n    <string name=\"failed_to_save_proxy_settings\">Proxy ayarları kaydedilemedi</string>\n    <string name=\"proxy_host_required\">Proxy ana bilgisayarı gerekli</string>\n    <string name=\"invalid_proxy_port\">Geçersiz proxy portu</string>\n    <string name=\"proxy_show_password\">Şifreyi göster</string>\n    <string name=\"proxy_hide_password\">Şifreyi gizle</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">Bu uygulamayı izle</string>\n    <string name=\"app_tracked_successfully\">Uygulama izleme listesine eklendi</string>\n    <string name=\"failed_to_track_app\">Uygulama izlenemedi: %1$s</string>\n    <string name=\"already_tracked\">Uygulama zaten izleniyor</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">GitHub ile giriş yap</string>\n    <string name=\"profile_sign_in_description\">Tam deneyimi keşfedin. Uygulamalarınızı yönetin, tercihlerinizi senkronize edin ve daha hızlı gezinin.</string>\n    <string name=\"profile_repos\">Repolar</string>\n    <string name=\"profile_login\">Giriş yap</string>\n    <string name=\"profile_stars_description\">GitHub'daki yıldızlı repolarınız</string>\n    <string name=\"profile_favourites_description\">Yerel olarak kaydedilen favori repolarınız</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">Oturum Süresi Doldu</string>\n    <string name=\"session_expired_message\">GitHub oturumunuzun süresi doldu veya token iptal edildi. Kimliği doğrulanmış özellikleri kullanmaya devam etmek için lütfen tekrar giriş yapın.</string>\n    <string name=\"session_expired_hint\">Sınırlı API istekleriyle misafir olarak gezinmeye devam edebilirsiniz.</string>\n    <string name=\"sign_in_again\">Tekrar Giriş Yap</string>\n    <string name=\"continue_as_guest\">Misafir olarak devam et</string>\n    <string name=\"logout_revocation_note\">Bu işlem yerel oturumunuzu ve önbellek verilerinizi temizleyecektir. Erişimi tamamen iptal etmek için GitHub Settings > Applications sayfasını ziyaret edin.</string>\n    <string name=\"auth_code_expires_in\">Kodun süresi %1$s sonra dolacak</string>\n    <string name=\"auth_error_code_expired\">Cihaz kodunun süresi doldu.</string>\n    <string name=\"auth_hint_try_again\">Yeni bir kod almak için lütfen tekrar giriş yapmayı deneyin.</string>\n    <string name=\"auth_hint_check_connection\">Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin.</string>\n    <string name=\"auth_hint_denied\">Yetkilendirme isteğini reddettiniz. İstemeden yaptıysanız tekrar deneyin.</string>\n\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">Devamını oku</string>\n    <string name=\"show_less\">Daha az göster</string>\n\n    <string name=\"share_repository\">Depoyu paylaş</string>\n    <string name=\"failed_to_share_link\">Bağlantı paylaşılamadı</string>\n    <string name=\"link_copied_to_clipboard\">Bağlantı panoya kopyalandı</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">Çevir</string>\n    <string name=\"translating\">Çevriliyor…</string>\n    <string name=\"show_original\">Orijinali göster</string>\n    <string name=\"translated_to\">%1$s diline çevrildi</string>\n    <string name=\"translate_to\">Şuna çevir…</string>\n    <string name=\"search_language\">Dil ara</string>\n    <string name=\"change_language\">Dili değiştir</string>\n    <string name=\"translation_failed\">Çeviri başarısız. Lütfen tekrar deneyin.</string>\n\n    <string name=\"open_github_link\">GitHub bağlantısını aç</string>\n    <string name=\"clipboard_link_detected\">Panoda GitHub bağlantısı algılandı</string>\n    <string name=\"auto_detect_clipboard_links\">Pano bağlantılarını otomatik algıla</string>\n    <string name=\"auto_detect_clipboard_description\">Arama açılırken panodan GitHub bağlantılarını otomatik olarak algıla</string>\n    <string name=\"detected_links\">Algılanan bağlantılar</string>\n    <string name=\"open_in_app\">Uygulamada aç</string>\n    <string name=\"no_github_link_in_clipboard\">Panoda GitHub bağlantısı bulunamadı</string>\n\n    <string name=\"storage\">Depolama</string>\n    <string name=\"clear_cache\">Önbelleği Temizle</string>\n    <string name=\"current_size\">Geçerli boyut:</string>\n    <string name=\"clear\">Temizle</string>\n\n    <string name=\"sponsor_title\">GitHub Store'u Destekle</string>\n    <string name=\"sponsor_button\">Projeyi destekle</string>\n    <string name=\"sponsor_hero_title\">Sevgiyle yapıldı,\\nkahveyle sürdürüldü</string>\n\n    <string name=\"sponsor_hero_subtitle\">GitHub Store 130.000+ indirme ve 7.700+ GitHub yıldızına ulaştı — %100 ücretsiz, reklamsız ve izleme yok.</string>\n\n    <string name=\"sponsor_personal_note\">Bu projeyi liseyi bitirirken tamamen tek başıma geliştiriyorum ve sürdürüyorum.</string>\n\n    <string name=\"sponsor_kodee_title\">GitHub Store için oy ver!</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store KotlinConf 2026 Golden Kodee Awards için aday gösterildi.</string>\n\n    <string name=\"sponsor_kodee_register\">1. Kayıt ol</string>\n    <string name=\"sponsor_kodee_vote\">2. Oy ver</string>\n\n    <string name=\"sponsor_kodee_deadline\">Oylama 22 Mart'ta kapanıyor</string>\n\n    <string name=\"sponsor_kodee_step1\">1. Platformda kayıt ol (Google ile devam et)</string>\n    <string name=\"sponsor_kodee_step2\">2. Aşağıdan Oy Ver'e dokun</string>\n    <string name=\"sponsor_kodee_step3\">3. Usmon Narzullayev'i bul ve Oy Ver'e tıkla</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">GitHub üzerinden tek seferlik veya düzenli destek</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">Hızlı tek seferlik destek</string>\n\n    <string name=\"sponsor_other_ways_title\">YARDIM ETMENİN DİĞER YOLLARI</string>\n\n    <string name=\"sponsor_star_repo\">Depoya yıldız ver</string>\n    <string name=\"sponsor_star_repo_desc\">Başkalarının GitHub Store'u keşfetmesine yardımcı olur</string>\n\n    <string name=\"sponsor_report_bugs\">Hata bildir</string>\n    <string name=\"sponsor_report_bugs_desc\">Uygulamayı herkes için daha iyi yapar</string>\n\n    <string name=\"sponsor_share\">Arkadaşlarınla paylaş</string>\n    <string name=\"sponsor_share_desc\">Diğer geliştiricilere duyur</string>\n\n    <string name=\"sponsor_thank_you\">Her destek — finansal olsun ya da olmasın — bu projeyi yaşatır. Teşekkürler!</string>\n\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">Kurulum</string>\n    <string name=\"installer_type_default\">Varsayılan</string>\n    <string name=\"installer_type_default_description\">Standart sistem kurulum penceresi</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">Onay gerektirmeyen sessiz kurulum</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku yüklü değil</string>\n    <string name=\"shizuku_status_not_running\">Shizuku çalışmıyor</string>\n    <string name=\"shizuku_status_permission_needed\">İzin gerekli</string>\n    <string name=\"shizuku_status_ready\">Hazır</string>\n    <string name=\"shizuku_grant_permission\">İzin ver</string>\n    <string name=\"shizuku_install_hint\">Sessiz kurulumu etkinleştirmek için Shizuku\\'yu yükleyin</string>\n    <string name=\"shizuku_start_hint\">Sessiz kurulumu etkinleştirmek için Shizuku\\'yu başlatın</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizuku kurulumu başarısız, standart yükleyici kullanılıyor</string>\n\n    <string name=\"auto_update_title\">Uygulamaları otomatik güncelle</string>\n    <string name=\"auto_update_description\">Shizuku aracılığıyla arka planda otomatik olarak güncellemeleri indirin ve yükleyin</string>\n\n    <string name=\"section_updates\">Güncellemeler</string>\n    <string name=\"update_check_interval_title\">Güncelleme kontrol aralığı</string>\n    <string name=\"update_check_interval_description\">Arka planda uygulama güncellemelerinin ne sıklıkla kontrol edileceği</string>\n    <string name=\"interval_3h\">3s</string>\n    <string name=\"interval_6h\">6s</string>\n    <string name=\"interval_12h\">12s</string>\n    <string name=\"interval_24h\">24s</string>\n\n    <string name=\"add_by_link\">Bağlantıyla ekle</string>\n    <string name=\"link_app_title\">Uygulamayı depoya bağla</string>\n    <string name=\"pick_installed_app\">GitHub deposuna bağlamak için yüklü bir uygulama seçin</string>\n    <string name=\"search_apps_hint\">Uygulama ara…</string>\n    <string name=\"enter_repo_url\">GitHub depo URL\\'si</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">Doğrulanıyor…</string>\n    <string name=\"link_and_track\">Bağla ve takip et</string>\n    <string name=\"checking_release\">Son sürüm kontrol ediliyor…</string>\n    <string name=\"downloading_for_verification\">Doğrulama için APK indiriliyor…</string>\n    <string name=\"verifying_signing_key\">İmza anahtarı doğrulanıyor…</string>\n    <string name=\"package_name_mismatch\">Paket adı uyuşmuyor: APK %1$s, ancak seçilen uygulama %2$s</string>\n    <string name=\"signing_key_mismatch_link\">İmza anahtarı uyuşmuyor: bu depodaki APK farklı bir geliştirici tarafından imzalanmış</string>\n    <string name=\"select_asset_title\">Yükleyici seçin</string>\n    <string name=\"select_asset_description\">Yüklü uygulamanızla doğrulamak için APK seçin</string>\n    <string name=\"download_failed\">İndirme başarısız</string>\n    <string name=\"export_apps\">Dışa aktar</string>\n    <string name=\"import_apps\">İçe aktar</string>\n    <string name=\"import_apps_title\">Uygulamaları içe aktar</string>\n    <string name=\"import_apps_description\">Takip edilen uygulamaları geri yüklemek için dışa aktarılan JSON\\'u yapıştırın</string>\n    <string name=\"import_apps_hint\">Dışa aktarılan JSON\\'u buraya yapıştırın…</string>\n    <string name=\"include_pre_releases_title\">Ön sürümleri dahil et</string>\n    <string name=\"include_pre_releases_description\">Güncelleme kontrolünde ön sürümleri takip edin. Devre dışı bırakıldığında, yalnızca kararlı sürümler dikkate alınır.</string>\n    <string name=\"confirm_uninstall_title\">Uygulama kaldırılsın mı?</string>\n    <string name=\"confirm_uninstall_message\">%1$s uygulamasını kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz ve uygulama verileri kaybolabilir.</string>\n    <string name=\"invalid_github_url\">Geçersiz GitHub URL\\'si. Biçim: github.com/owner/repo</string>\n    <string name=\"repo_not_found\">Depo bulunamadı: %1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub API istek sınırı aşıldı. Daha sonra tekrar deneyin.</string>\n    <string name=\"failed_to_link\">Bağlama başarısız: %1$s</string>\n    <string name=\"failed_to_load_apps\">Yüklü uygulamalar yüklenemedi</string>\n    <string name=\"app_linked_success\">%1$s, %2$s/%3$s ile bağlandı</string>\n    <string name=\"export_failed\">Dışa aktarma başarısız: %1$s</string>\n    <string name=\"import_failed\">İçe aktarma başarısız: %1$s</string>\n    <string name=\"imported_apps_summary\">%1$d uygulama içe aktarıldı</string>\n    <string name=\"imported_skipped\">, %1$d atlandı</string>\n    <string name=\"imported_failed\">, %1$d başarısız</string>\n    <string name=\"signing_key_changed_title\">İmza anahtarı değişti</string>\n    <string name=\"signing_key_changed_message\">Bu uygulamanın imza sertifikası ilk kurulumdan bu yana değişti.\\n\\nBu, geliştiricinin imza anahtarını değiştirdiği veya dosyanın değiştirilmiş olabileceği anlamına gelebilir.\\n\\nBeklenen: %1$s\\nAlınan: %2$s</string>\n    <string name=\"install_anyway\">Yine de yükle</string>\n    <string name=\"verified_build\">Doğrulanmış yapı</string>\n    <string name=\"checking_attestation\">Kontrol ediliyor\\u2026</string>\n    <string name=\"assets_title\">Dosyalar</string>\n    <string name=\"no_assets_selected\">Dosya yok</string>\n    <string name=\"no_assets_in_list\">Bu sürümle ilişkili dosya yok</string>\n    <string name=\"assets_selection_label\">Dosya seçeneği belirleyin</string>\n    <string name=\"multiple_assets_info_dialog_title\">Birden fazla dosya mevcut</string>\n    <string name=\"multiple_assets_info_dialog_text\">Bu sürüm için birden fazla kurulabilir dosya mevcut. Listeyi inceleyin ve cihazınıza uygun olanı seçin.</string>\n    <string name=\"icon_content_description_info\">Bilgi</string>\n    <string name=\"translation_error_retry\">Tekrar dene</string>\n    <string name=\"translated_from\">Otomatik algılanan: %1$s</string>\n    <string name=\"select_language\">Dil seçin</string>\n    <string name=\"update_package_mismatch\">Paket uyumsuzluğu: APK %1$s, ancak yüklü uygulama %2$s. Güncelleme engellendi.</string>\n    <string name=\"update_signing_key_mismatch\">İmza anahtarı uyumsuzluğu: güncelleme farklı bir geliştirici tarafından imzalanmış. Güncelleme engellendi.</string>\n    <string name=\"liquid_glass_option_title\">Sıvı Cam Efekti</string>\n    <string name=\"liquid_glass_option_description\">Arayüzü pürüzsüz cam benzeri bir görünümle geliştirin</string>\n\n    <string name=\"hide_seen_title\">Görülen depoları gizle</string>\n    <string name=\"hide_seen_description\">Zaten görüntülediğiniz depoları keşif akışlarından gizleyin</string>\n    <string name=\"clear_seen_history\">Görüntüleme geçmişini temizle</string>\n    <string name=\"clear_seen_history_description\">Tüm görüntülenen depoları sıfırlayarak akışlarda tekrar görünmelerini sağlayın</string>\n    <string name=\"seen_history_cleared\">Görüntüleme geçmişi temizlendi</string>\n    <string name=\"seen_badge\">Görüntülendi</string>\n</resources>\n"
  },
  {
    "path": "core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml",
    "content": "<resources>\n    <string name=\"app_name\">GitHub Store</string>\n\n    <string name=\"installed_apps\">已安装的应用</string>\n    <string name=\"navigate_back\">返回</string>\n    <string name=\"check_for_updates\">检查更新</string>\n\n    <string name=\"cannot_launch\">无法启动 %1$s</string>\n    <string name=\"failed_to_open\">无法打开 %1$s</string>\n    <string name=\"failed_to_update\">更新 %1$s 失败：%2$s</string>\n    <string name=\"update_failed\">更新失败</string>\n    <string name=\"update_all_failed\">全部更新失败：%1$s</string>\n    <string name=\"all_apps_updated_successfully\">所有应用已成功更新</string>\n    <string name=\"no_updates_available\">暂无可用更新</string>\n\n    <string name=\"search_your_apps\">搜索应用</string>\n    <string name=\"no_apps_found\">未找到应用</string>\n\n    <string name=\"update_all\">全部更新</string>\n    <string name=\"update\">更新</string>\n    <string name=\"open\">打开</string>\n    <string name=\"cancel\">取消</string>\n\n    <string name=\"checking\">正在检查…</string>\n    <string name=\"updated_successfully\">更新成功</string>\n    <string name=\"error_with_message\">错误：%1$s</string>\n\n    <string name=\"updating_x_of_y\">正在更新 %1$d / %2$d</string>\n    <string name=\"currently_updating\">当前：%1$s</string>\n\n    <string name=\"percent\">%1$d%%</string>\n\n    <string name=\"waiting_for_authorization\">正在等待授权…</string>\n    <string name=\"signed_in\">登录成功！</string>\n    <string name=\"redirecting_message\">现在可以使用应用。正在跳转…</string>\n    <string name=\"try_again\">重试</string>\n\n    <string name=\"auth_error_with_message\">错误：%1$s</string>\n\n    <string name=\"enter_code_on_github\">请在 GitHub 上输入此代码：</string>\n    <string name=\"copy_code\">复制代码</string>\n    <string name=\"open_github\">打开 GitHub</string>\n\n    <string name=\"unlock_full_experience\">解锁完整\\n体验</string>\n\n    <string name=\"more_requests\">更多请求</string>\n    <string name=\"more_requests_description\">\n        登录以获得更高的 API 限额，避免中断。\n    </string>\n\n    <string name=\"sign_in_with_github\">使用 GitHub 登录</string>\n\n    <string name=\"error_cancelled\">已取消</string>\n    <string name=\"error_unknown\">未知错误</string>\n\n    <string name=\"language_label\">语言：</string>\n\n    <string name=\"discover_repositories\">发现仓库</string>\n    <string name=\"search_repositories_hint\">搜索仓库、描述…</string>\n\n    <string name=\"filter_by_language\">按语言筛选</string>\n\n    <string name=\"results_found\">找到 %1$d 个结果</string>\n\n    <string name=\"retry\">重试</string>\n\n    <string name=\"sort_by\">排序方式</string>\n    <string name=\"close\">关闭</string>\n\n    <string name=\"sort_most_stars\">最多星标</string>\n    <string name=\"sort_most_forks\">最多分叉</string>\n    <string name=\"sort_best_match\">最佳匹配</string>\n\n    <string name=\"sort_order_descending\">降序</string>\n    <string name=\"sort_order_ascending\">升序</string>\n    <string name=\"sort_label\">排序</string>\n\n    <string name=\"language_all\">所有语言</string>\n    <string name=\"language_kotlin\">Kotlin</string>\n    <string name=\"language_java\">Java</string>\n    <string name=\"language_javascript\">JavaScript</string>\n    <string name=\"language_typescript\">TypeScript</string>\n    <string name=\"language_python\">Python</string>\n    <string name=\"language_swift\">Swift</string>\n    <string name=\"language_rust\">Rust</string>\n    <string name=\"language_go\">Go</string>\n    <string name=\"language_csharp\">C#</string>\n    <string name=\"language_cpp\">C++</string>\n    <string name=\"language_c\">C</string>\n    <string name=\"language_dart\">Dart</string>\n    <string name=\"language_ruby\">Ruby</string>\n    <string name=\"language_php\">PHP</string>\n\n    <string name=\"search_failed\">搜索失败</string>\n    <string name=\"no_repositories_found\">未找到仓库</string>\n\n    <string name=\"profile_title\">个人资料</string>\n\n    <string name=\"section_appearance\">外观</string>\n    <string name=\"section_about\">关于</string>\n    <string name=\"section_network\">网络</string>\n\n    <string name=\"theme_color\">主题颜色</string>\n    <string name=\"amoled_black_theme\">AMOLED 黑色主题</string>\n    <string name=\"amoled_black_description\">深色模式下的纯黑背景</string>\n    <string name=\"selected_color\">已选择颜色：%1$s</string>\n\n    <string name=\"version\">版本</string>\n    <string name=\"help_support\">帮助与支持</string>\n\n    <string name=\"logout\">退出登录</string>\n\n    <string name=\"logout_success\">已成功退出，正在跳转…</string>\n    <string name=\"cache_cleared\">缓存已成功清除</string>\n\n    <string name=\"warning\">警告！</string>\n    <string name=\"logout_confirmation\">确定要退出登录吗？</string>\n\n    <string name=\"theme_dynamic\">动态</string>\n    <string name=\"theme_ocean\">海洋</string>\n    <string name=\"theme_purple\">紫色</string>\n    <string name=\"theme_forest\">森林</string>\n    <string name=\"theme_slate\">石板</string>\n    <string name=\"theme_amber\">琥珀</string>\n\n    <string name=\"error_loading_details\">加载详情失败</string>\n    <string name=\"about_this_app\">关于此应用</string>\n    <string name=\"install_logs\">安装日志</string>\n    <string name=\"author\">作者</string>\n    <string name=\"whats_new\">更新内容</string>\n\n    <string name=\"installed\">已安装</string>\n    <string name=\"update_available\">有可用更新</string>\n    <string name=\"install_latest\">安装最新版本</string>\n    <string name=\"reinstall\">重新安装</string>\n\n    <string name=\"downloading\">正在下载</string>\n    <string name=\"updating\">正在更新</string>\n    <string name=\"verifying\">正在验证</string>\n    <string name=\"installing\">正在安装</string>\n\n    <string name=\"profile\">个人资料</string>\n    <string name=\"forks\">分支</string>\n    <string name=\"stars\">星标</string>\n    <string name=\"issues\">问题</string>\n\n    <string name=\"by_author\">由 %1$s</string>\n    <string name=\"installed_version\">• 已安装：%1$s</string>\n    <string name=\"architecture_compatible\">架构兼容</string>\n    <string name=\"update_to_version\">更新到 %1$s</string>\n    <string name=\"report_issue\">报告问题</string>\n\n    <string name=\"error_load_details\">无法加载详情</string>\n    <string name=\"installer_saved_downloads\">安装程序已保存到下载文件夹</string>\n\n    <string name=\"log_download_started\">开始下载</string>\n    <string name=\"log_downloaded\">已下载</string>\n    <string name=\"log_update_started\">开始更新</string>\n    <string name=\"log_installed\">已安装</string>\n    <string name=\"log_updated\">已更新</string>\n    <string name=\"log_cancelled\">已取消</string>\n    <string name=\"log_install_started\">开始安装</string>\n    <string name=\"log_error\">错误</string>\n    <string name=\"log_error_with_message\">错误：%1$s</string>\n\n    <string name=\"log_prepare_appmanager\">正在为 AppManager 准备</string>\n    <string name=\"log_opened_appmanager\">已在 AppManager 中打开</string>\n    <string name=\"log_permission_blocked\">安装权限被设备策略阻止</string>\n    <string name=\"log_opened_external_installer\">已在外部安装器中打开</string>\n    <string name=\"install_permission_unavailable\">安装权限不可用</string>\n    <string name=\"install_permission_blocked_message\">APK已成功下载，但此设备不允许直接安装。是否使用外部安装器打开？</string>\n    <string name=\"open_with_external_installer\">使用外部安装器打开</string>\n    <string name=\"external_installer_description\">使用第三方应用安装APK</string>\n\n    <string name=\"error_generic\">错误：%1$s</string>\n    <string name=\"error_asset_not_supported\">不支持的文件类型 .%1$s</string>\n    <string name=\"error_file_not_found\">未找到下载的文件</string>\n\n    <string name=\"home_category_trending\">热门</string>\n    <string name=\"home_category_hot_release\">热门发布</string>\n    <string name=\"home_category_most_popular\">最受欢迎</string>\n\n    <string name=\"home_finding_repositories\">正在查找仓库…</string>\n    <string name=\"home_loading_more\">加载中…</string>\n    <string name=\"home_no_more_repositories\">没有更多仓库了</string>\n    <string name=\"home_retry\">重试</string>\n    <string name=\"home_failed_to_load_repositories\">加载仓库失败</string>\n    <string name=\"home_view_details\">查看详情</string>\n\n    <string name=\"updated_just_now\">刚刚更新</string>\n    <string name=\"updated_hours_ago\">%1$d 小时前更新</string>\n    <string name=\"updated_yesterday\">昨天更新</string>\n    <string name=\"updated_days_ago\">%1$d 天前更新</string>\n    <string name=\"updated_on_date\">%1$s 更新</string>\n\n    <string name=\"rate_limit_exceeded\">请求次数已达上限</string>\n    <string name=\"rate_limit_used_all\">已用完 %1$d 次 API 请求。</string>\n    <string name=\"rate_limit_used_all_free\">已用完 %1$d 次免费 API 请求。</string>\n    <string name=\"rate_limit_resets_in_minutes\">%1$d 分钟后重置</string>\n    <string name=\"rate_limit_tip_sign_in\">💡 登录后每小时可使用 5,000 次请求，而不是 60 次！</string>\n    <string name=\"rate_limit_sign_in\">登录</string>\n    <string name=\"rate_limit_ok\">确定</string>\n    <string name=\"rate_limit_close\">关闭</string>\n\n    <string name=\"system_font\">系统字体</string>\n    <string name=\"system_font_description\">使用设备字体以提高可读性</string>\n\n    <string name=\"theme_light\">浅色</string>\n    <string name=\"theme_dark\">深色</string>\n    <string name=\"theme_system\">跟随系统</string>\n\n    <string name=\"added_to_favourites\">已添加到收藏</string>\n    <string name=\"removed_from_favourites\">已从收藏中移除</string>\n    <string name=\"add_to_favourites\">添加到收藏</string>\n    <string name=\"remove_from_favourites\">从收藏中移除</string>\n    <string name=\"favourites\">收藏</string>\n\n    <string name=\"added_just_now\">刚刚添加</string>\n    <string name=\"added_hours_ago\">%1$d 小时前添加</string>\n    <string name=\"added_yesterday\">昨天添加</string>\n    <string name=\"added_days_ago\">%1$d 天前添加</string>\n    <string name=\"added_on_date\">%1$s 添加</string>\n\n    <string name=\"starred_repositories\">已收藏的仓库</string>\n    <string name=\"repository_starred\">仓库已收藏</string>\n    <string name=\"repository_not_starred\">仓库未收藏</string>\n    <string name=\"star_from_github\">你可以在 GitHub 上收藏该仓库</string>\n    <string name=\"unstar_from_github\">你可以在 GitHub 上取消收藏该仓库</string>\n\n    <string name=\"sign_in_required\">需要登录</string>\n    <string name=\"no_starred_repos\">没有收藏的仓库</string>\n    <string name=\"sign_in_with_github_for_stars\">使用 GitHub 登录以查看收藏的仓库</string>\n    <string name=\"star_repos_hint\">在 GitHub 上收藏有可安装版本的仓库以在此查看</string>\n    <string name=\"last_synced\">上次同步</string>\n    <string name=\"just_now\">刚刚</string>\n    <string name=\"minutes_ago\">%1$d 分钟前</string>\n    <string name=\"hours_ago\">%1$d 小时前</string>\n    <string name=\"days_ago\">%1$d 天前</string>\n    <string name=\"dismiss\">关闭</string>\n    <string name=\"sync_starred_failed\">同步收藏的仓库失败</string>\n\n    <string name=\"developer_profile_title\">开发者简介</string>\n    <string name=\"open_developer_profile\">打开开发者个人资料</string>\n\n    <!-- Developer Profile - General -->\n    <string name=\"failed_to_load_repositories\">加载仓库失败</string>\n    <string name=\"failed_to_load_profile\">加载个人资料失败</string>\n\n    <!-- Developer Profile - Stats -->\n    <string name=\"repositories\">仓库</string>\n    <string name=\"followers\">关注者</string>\n    <string name=\"following\">正在关注</string>\n\n    <!-- Developer Profile - Filter & Sort -->\n    <string name=\"search_repositories\">搜索仓库…</string>\n    <string name=\"clear_search\">清除搜索</string>\n    <string name=\"filter_all\">全部</string>\n    <string name=\"filter_with_releases\">有发布版</string>\n    <string name=\"filter_installed\">已安装</string>\n    <string name=\"filter_favorites\">收藏</string>\n    <string name=\"sort\">排序</string>\n    <string name=\"sort_recently_updated\">最近更新</string>\n    <string name=\"sort_name\">名称</string>\n\n    <!-- Developer Profile - Repository Count -->\n    <string name=\"repository_singular\">个仓库</string>\n    <string name=\"repositories_plural\">个仓库</string>\n    <string name=\"showing_x_of_y_repositories\">显示 %2$d 个中的 %1$d 个仓库</string>\n\n    <!-- Developer Profile - Empty States -->\n    <string name=\"no_repos_with_releases\">没有可安装发布版的仓库</string>\n    <string name=\"no_installed_repos\">没有已安装的仓库</string>\n    <string name=\"no_favorite_repos\">没有收藏的仓库</string>\n\n    <!-- Developer Profile - Repository Item -->\n    <string name=\"updated_x_ago\">%1$s前更新</string>\n    <string name=\"has_release\">有发布版</string>\n\n    <!-- Developer Profile - Relative Time -->\n    <string name=\"time_years_ago\">%1$d 年前</string>\n    <string name=\"time_months_ago\">%1$d 个月前</string>\n    <string name=\"time_days_ago\">%1$d 天前</string>\n    <string name=\"time_hours_ago\">%1$d 小时前</string>\n    <string name=\"time_minutes_ago\">%1$d 分钟前</string>\n\n    <!-- Developer Profile - Count Formatting -->\n    <string name=\"count_millions\">%1$dM</string>\n    <string name=\"count_thousands\">%1$dk</string>\n\n    <string name=\"released_just_now\">刚刚发布</string>\n    <string name=\"released_hours_ago\">%1$d 小时前发布</string>\n    <string name=\"released_yesterday\">昨天发布</string>\n    <string name=\"released_days_ago\">%1$d 天前发布</string>\n    <string name=\"released_on_date\">发布于 %1$s</string>\n\n    <string name=\"bottom_nav_home_title\">首页</string>\n    <string name=\"bottom_nav_search_title\">搜索</string>\n    <string name=\"bottom_nav_apps_title\">应用</string>\n    <string name=\"bottom_nav_profile_title\">个人资料</string>\n\n    <string name=\"forked_repository\">分叉</string>\n\n    <string name=\"category_stable\">稳定版</string>\n    <string name=\"category_pre_release\">预发布</string>\n    <string name=\"category_all\">全部</string>\n    <string name=\"select_version\">选择版本</string>\n    <string name=\"pre_release_badge\">预发布</string>\n    <string name=\"no_version_selected\">未选择版本</string>\n    <string name=\"versions_title\">版本</string>\n\n    <!-- Navigation & actions -->\n    <string name=\"open_repository\">打开仓库</string>\n    <string name=\"open_in_browser\">在浏览器中打开</string>\n    <string name=\"cancel_download\">取消下载</string>\n    <string name=\"show_install_options\">显示安装选项</string>\n\n    <!-- Errors & states -->\n    <string name=\"no_description\">暂无描述。</string>\n    <string name=\"no_release_notes\">暂无发行说明。</string>\n\n    <!-- Install status -->\n    <string name=\"not_available\">不可用</string>\n    <string name=\"update_app\">更新应用</string>\n    <string name=\"pending_install\">等待安装</string>\n\n    <!-- Install helpers -->\n    <string name=\"open_in_obtainium\">在 Obtainium 中打开</string>\n    <string name=\"obtainium_description\">自动管理更新</string>\n    <string name=\"inspect_with_appmanager\">使用 AppManager 检查</string>\n    <string name=\"appmanager_description\">检查权限、追踪器和安全性</string>\n\n    <!-- Uninstall / Open -->\n    <string name=\"uninstall\">卸载</string>\n    <string name=\"open_app\">打开</string>\n    <string name=\"downgrade_requires_uninstall\">降级需要先卸载</string>\n    <string name=\"downgrade_warning_message\">安装版本 %1$s 需要先卸载当前版本（%2$s）。应用数据将丢失。</string>\n    <string name=\"uninstall_first\">先卸载</string>\n    <string name=\"install_version\">安装 %1$s</string>\n    <string name=\"failed_to_open_app\">无法打开 %1$s</string>\n    <string name=\"failed_to_uninstall\">无法卸载 %1$s</string>\n\n    <!-- Version picker -->\n    <string name=\"latest_badge\">最新</string>\n\n    <!-- Apps feature - Last checked -->\n    <string name=\"last_checked\">上次检查：%1$s</string>\n    <string name=\"last_checked_never\">从未检查</string>\n    <string name=\"last_checked_just_now\">刚刚</string>\n    <string name=\"last_checked_minutes_ago\">%1$d 分钟前</string>\n    <string name=\"last_checked_hours_ago\">%1$d 小时前</string>\n    <string name=\"checking_for_updates\">正在检查更新…</string>\n\n    <!-- Proxy -->\n    <string name=\"proxy_type\">代理类型</string>\n    <string name=\"proxy_none\">无</string>\n    <string name=\"proxy_system\">系统代理</string>\n    <string name=\"proxy_http\">HTTP</string>\n    <string name=\"proxy_socks\">SOCKS</string>\n    <string name=\"proxy_host\">主机地址</string>\n    <string name=\"proxy_port\">端口</string>\n    <string name=\"proxy_username\">用户名（可选）</string>\n    <string name=\"proxy_password\">密码（可选）</string>\n    <string name=\"proxy_save\">保存代理</string>\n    <string name=\"proxy_saved\">代理设置已保存</string>\n    <string name=\"proxy_system_description\">使用设备的代理设置</string>\n    <string name=\"proxy_port_error\">端口必须为 1–65535</string>\n    <string name=\"proxy_none_description\">直连，不使用代理</string>\n    <string name=\"failed_to_save_proxy_settings\">无法保存代理设置</string>\n    <string name=\"proxy_host_required\">必须填写代理主机</string>\n    <string name=\"invalid_proxy_port\">无效的代理端口</string>\n    <string name=\"proxy_show_password\">显示密码</string>\n    <string name=\"proxy_hide_password\">隐藏密码</string>\n\n\n    <!-- Track app feature -->\n    <string name=\"track_this_app\">跟踪此应用</string>\n    <string name=\"app_tracked_successfully\">应用已添加到跟踪列表</string>\n    <string name=\"failed_to_track_app\">跟踪应用失败：%1$s</string>\n    <string name=\"already_tracked\">该应用已在跟踪中</string>\n\n    <!-- Profile section -->\n    <string name=\"profile_sign_in_title\">登录 GitHub</string>\n    <string name=\"profile_sign_in_description\">解锁完整体验。管理您的应用、同步偏好设置，更快地浏览。</string>\n    <string name=\"profile_repos\">仓库</string>\n    <string name=\"profile_login\">登录</string>\n    <string name=\"profile_stars_description\">您在 GitHub 上收藏的仓库</string>\n    <string name=\"profile_favourites_description\">本地保存的收藏仓库</string>\n\n    <!-- Session expired dialog -->\n    <string name=\"session_expired_title\">会话已过期</string>\n    <string name=\"session_expired_message\">您的 GitHub 会话已过期或令牌已被撤销。请重新登录以继续使用认证功能。</string>\n    <string name=\"session_expired_hint\">您仍可以访客身份浏览，但 API 请求次数有限。</string>\n    <string name=\"sign_in_again\">重新登录</string>\n    <string name=\"continue_as_guest\">以访客身份继续</string>\n    <string name=\"logout_revocation_note\">这将清除您的本地会话和缓存数据。要完全撤销访问权限，请访问 GitHub Settings > Applications。</string>\n    <string name=\"auth_code_expires_in\">验证码将在 %1$s 后过期</string>\n    <string name=\"auth_error_code_expired\">设备验证码已过期。</string>\n    <string name=\"auth_hint_try_again\">请重新登录以获取新的验证码。</string>\n    <string name=\"auth_hint_check_connection\">请检查您的网络连接并重试。</string>\n    <string name=\"auth_hint_denied\">您拒绝了授权请求。如果是误操作，请重试。</string>\n    <!-- Read More / Show Less -->\n    <string name=\"read_more\">阅读更多</string>\n    <string name=\"show_less\">收起</string>\n\n    <string name=\"share_repository\">分享仓库</string>\n    <string name=\"failed_to_share_link\">无法分享链接</string>\n    <string name=\"link_copied_to_clipboard\">链接已复制到剪贴板</string>\n\n    <!-- Translation feature -->\n    <string name=\"translate\">翻译</string>\n    <string name=\"translating\">翻译中…</string>\n    <string name=\"show_original\">显示原文</string>\n    <string name=\"translated_to\">已翻译为%1$s</string>\n    <string name=\"translate_to\">翻译为…</string>\n    <string name=\"search_language\">搜索语言</string>\n    <string name=\"change_language\">更改语言</string>\n    <string name=\"translation_failed\">翻译失败，请重试。</string>\n\n    <string name=\"open_github_link\">打开 GitHub 链接</string>\n    <string name=\"clipboard_link_detected\">在剪贴板中检测到 GitHub 链接</string>\n    <string name=\"auto_detect_clipboard_links\">自动检测剪贴板链接</string>\n    <string name=\"auto_detect_clipboard_description\">打开搜索时自动检测剪贴板中的 GitHub 链接</string>\n    <string name=\"detected_links\">检测到的链接</string>\n    <string name=\"open_in_app\">在应用中打开</string>\n    <string name=\"no_github_link_in_clipboard\">剪贴板中未找到 GitHub 链接</string>\n\n    <string name=\"storage\">存储</string>\n    <string name=\"clear_cache\">清除缓存</string>\n    <string name=\"current_size\">当前大小：</string>\n    <string name=\"clear\">清除</string>\n\n    <string name=\"sponsor_title\">支持 GitHub Store</string>\n    <string name=\"sponsor_button\">支持项目</string>\n    <string name=\"sponsor_hero_title\">用爱构建，\\n用咖啡维护</string>\n    <string name=\"sponsor_hero_subtitle\">GitHub Store 已达到 130,000+ 下载和 7,700+ GitHub 星标 —— 100% 免费，无广告，无跟踪。</string>\n\n    <string name=\"sponsor_personal_note\">我在完成高中学业的同时，完全独立开发并维护这个项目。你的支持——哪怕很小——都能帮助保持应用无 bug、支付基础设施费用，并实现你们请求的功能。</string>\n\n    <string name=\"sponsor_kodee_title\">为 GitHub Store 投票！</string>\n    <string name=\"sponsor_kodee_subtitle\">GitHub Store 已被提名为 KotlinConf 2026 Golden Kodee Awards。</string>\n\n    <string name=\"sponsor_kodee_register\">1. 注册</string>\n    <string name=\"sponsor_kodee_vote\">2. 投票</string>\n    <string name=\"sponsor_kodee_deadline\">投票截止：3月22日</string>\n\n    <string name=\"sponsor_kodee_step1\">1. 在奖项平台注册（使用 Google 登录）</string>\n    <string name=\"sponsor_kodee_step2\">2. 点击下方“投票”</string>\n    <string name=\"sponsor_kodee_step3\">3. 找到 Usmon Narzullayev 并点击投票</string>\n\n    <string name=\"sponsor_github_sponsors\">GitHub Sponsors</string>\n    <string name=\"sponsor_github_sponsors_desc\">通过 GitHub 的一次性或持续支持</string>\n\n    <string name=\"sponsor_buy_me_coffee\">Buy Me a Coffee</string>\n    <string name=\"sponsor_buy_me_coffee_desc\">快速一次性支持</string>\n\n    <string name=\"sponsor_other_ways_title\">其他帮助方式</string>\n\n    <string name=\"sponsor_star_repo\">为仓库加星</string>\n    <string name=\"sponsor_star_repo_desc\">帮助更多人发现 GitHub Store</string>\n\n    <string name=\"sponsor_report_bugs\">报告问题</string>\n    <string name=\"sponsor_report_bugs_desc\">让应用变得更好</string>\n\n    <string name=\"sponsor_share\">分享给朋友</string>\n    <string name=\"sponsor_share_desc\">向其他开发者传播</string>\n\n    <string name=\"sponsor_thank_you\">任何形式的支持——无论是否金钱——都让这个项目继续存在。谢谢！</string>\n\n    <!-- Installation method settings -->\n    <string name=\"section_installation\">安装</string>\n    <string name=\"installer_type_default\">默认</string>\n    <string name=\"installer_type_default_description\">标准系统安装对话框</string>\n    <string name=\"installer_type_shizuku\">Shizuku</string>\n    <string name=\"installer_type_shizuku_description\">无需确认的静默安装</string>\n    <string name=\"shizuku_status_not_installed\">Shizuku 未安装</string>\n    <string name=\"shizuku_status_not_running\">Shizuku 未运行</string>\n    <string name=\"shizuku_status_permission_needed\">需要权限</string>\n    <string name=\"shizuku_status_ready\">就绪</string>\n    <string name=\"shizuku_grant_permission\">授予权限</string>\n    <string name=\"shizuku_install_hint\">安装 Shizuku 以启用静默安装</string>\n    <string name=\"shizuku_start_hint\">启动 Shizuku 以启用静默安装</string>\n    <string name=\"shizuku_install_failed_fallback\">Shizuku 安装失败，正在使用标准安装程序</string>\n\n    <string name=\"auto_update_title\">自动更新应用</string>\n    <string name=\"auto_update_description\">通过 Shizuku 在后台自动下载并安装更新</string>\n\n    <string name=\"section_updates\">更新</string>\n    <string name=\"update_check_interval_title\">更新检查间隔</string>\n    <string name=\"update_check_interval_description\">在后台检查应用更新的频率</string>\n    <string name=\"interval_3h\">3小时</string>\n    <string name=\"interval_6h\">6小时</string>\n    <string name=\"interval_12h\">12小时</string>\n    <string name=\"interval_24h\">24小时</string>\n\n    <string name=\"add_by_link\">通过链接添加</string>\n    <string name=\"link_app_title\">将应用链接到仓库</string>\n    <string name=\"pick_installed_app\">选择一个已安装的应用以链接到GitHub仓库</string>\n    <string name=\"search_apps_hint\">搜索应用…</string>\n    <string name=\"enter_repo_url\">GitHub仓库URL</string>\n    <string name=\"repo_url_hint\">github.com/owner/repo</string>\n    <string name=\"validating_repo\">验证中…</string>\n    <string name=\"link_and_track\">链接并追踪</string>\n    <string name=\"checking_release\">检查最新版本…</string>\n    <string name=\"downloading_for_verification\">正在下载APK进行验证…</string>\n    <string name=\"verifying_signing_key\">正在验证签名密钥…</string>\n    <string name=\"package_name_mismatch\">包名不匹配：APK为%1$s，但所选应用为%2$s</string>\n    <string name=\"signing_key_mismatch_link\">签名密钥不匹配：此仓库中的APK由不同的开发者签名</string>\n    <string name=\"select_asset_title\">选择安装包</string>\n    <string name=\"select_asset_description\">选择APK以与已安装的应用进行验证</string>\n    <string name=\"download_failed\">下载失败</string>\n    <string name=\"export_apps\">导出</string>\n    <string name=\"import_apps\">导入</string>\n    <string name=\"import_apps_title\">导入应用</string>\n    <string name=\"import_apps_description\">粘贴导出的JSON以恢复跟踪的应用</string>\n    <string name=\"import_apps_hint\">在此粘贴导出的JSON…</string>\n    <string name=\"include_pre_releases_title\">包含预发布版本</string>\n    <string name=\"include_pre_releases_description\">检查更新时跟踪预发布版本。禁用后，仅考虑稳定版本。</string>\n    <string name=\"confirm_uninstall_title\">卸载应用？</string>\n    <string name=\"confirm_uninstall_message\">确定要卸载%1$s吗？此操作无法撤销，应用数据可能会丢失。</string>\n    <string name=\"invalid_github_url\">无效的GitHub URL。请使用格式：github.com/owner/repo</string>\n    <string name=\"repo_not_found\">未找到仓库：%1$s/%2$s</string>\n    <string name=\"rate_limit_try_again\">GitHub API请求限制已超出。请稍后重试。</string>\n    <string name=\"failed_to_link\">链接失败：%1$s</string>\n    <string name=\"failed_to_load_apps\">无法加载已安装的应用</string>\n    <string name=\"app_linked_success\">%1$s已链接到%2$s/%3$s</string>\n    <string name=\"export_failed\">导出失败：%1$s</string>\n    <string name=\"import_failed\">导入失败：%1$s</string>\n    <string name=\"imported_apps_summary\">已导入%1$d个应用</string>\n    <string name=\"imported_skipped\">，%1$d个已跳过</string>\n    <string name=\"imported_failed\">，%1$d个失败</string>\n    <string name=\"signing_key_changed_title\">签名密钥已更改</string>\n    <string name=\"signing_key_changed_message\">此应用的签名证书自首次安装以来已更改。\\n\\n这可能意味着开发者更换了签名密钥，或者二进制文件可能已被篡改。\\n\\n预期：%1$s\\n收到：%2$s</string>\n    <string name=\"install_anyway\">仍然安装</string>\n    <string name=\"verified_build\">已验证的构建</string>\n    <string name=\"checking_attestation\">检查中\\u2026</string>\n    <string name=\"assets_title\">资源</string>\n    <string name=\"no_assets_selected\">无资源</string>\n    <string name=\"no_assets_in_list\">此版本没有关联的资源</string>\n    <string name=\"assets_selection_label\">选择资源选项</string>\n    <string name=\"multiple_assets_info_dialog_title\">多个资源可用</string>\n    <string name=\"multiple_assets_info_dialog_text\">此版本有多个可安装文件。请查看列表并选择适合您设备的文件。</string>\n    <string name=\"icon_content_description_info\">信息</string>\n    <string name=\"translation_error_retry\">重试</string>\n    <string name=\"translated_from\">自动检测：%1$s</string>\n    <string name=\"select_language\">选择语言</string>\n    <string name=\"update_package_mismatch\">包名不匹配：APK 为 %1$s，但已安装的应用为 %2$s。更新已阻止。</string>\n    <string name=\"update_signing_key_mismatch\">签名密钥不匹配：更新由不同的开发者签名。更新已阻止。</string>\n    <string name=\"liquid_glass_option_title\">液态玻璃效果</string>\n    <string name=\"liquid_glass_option_description\">以流畅的玻璃质感提升界面外观</string>\n\n    <string name=\"hide_seen_title\">隐藏已浏览的仓库</string>\n    <string name=\"hide_seen_description\">在发现信息流中隐藏你已经浏览过的仓库</string>\n    <string name=\"clear_seen_history\">清除浏览记录</string>\n    <string name=\"clear_seen_history_description\">重置所有已浏览的仓库，使其重新出现在信息流中</string>\n    <string name=\"seen_history_cleared\">浏览记录已清除</string>\n    <string name=\"seen_badge\">已浏览</string>\n</resources>"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt",
    "content": "package zed.rainxch.core.presentation.components\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun ExpressiveCard(\n    modifier: Modifier = Modifier,\n    onClick: (() -> Unit)? = null,\n    content: @Composable () -> Unit,\n) {\n    if (onClick != null) {\n        ElevatedCard(\n            modifier = modifier.fillMaxWidth(),\n            colors =\n                CardDefaults.elevatedCardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                ),\n            onClick = onClick,\n            shape = RoundedCornerShape(32.dp),\n            content = { content() },\n        )\n    } else {\n        ElevatedCard(\n            modifier = modifier.fillMaxWidth(),\n            colors =\n                CardDefaults.elevatedCardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                ),\n            shape = RoundedCornerShape(32.dp),\n            content = { content() },\n        )\n    }\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/GitHubStoreImage.kt",
    "content": "package zed.rainxch.core.presentation.components\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Warning\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.coil3.CoilImage\nimport com.skydoves.landscapist.components.rememberImageComponent\nimport com.skydoves.landscapist.crossfade.CrossfadePlugin\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun GitHubStoreImage(\n    imageModel: () -> Any?,\n    modifier: Modifier = Modifier,\n) {\n    CoilImage(\n        imageModel = imageModel,\n        modifier = modifier,\n        loading = {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center,\n            ) {\n                CircularWavyProgressIndicator()\n            }\n        },\n        failure = {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center,\n            ) {\n                Icon(\n                    imageVector = Icons.Default.Warning,\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.error,\n                    modifier = Modifier.fillMaxSize(.5f),\n                )\n            }\n        },\n        component =\n            rememberImageComponent {\n                CrossfadePlugin()\n            },\n    )\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/GithubStoreButton.kt",
    "content": "package zed.rainxch.core.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun GithubStoreButton(\n    text: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    icon: (@Composable () -> Unit)? = null,\n    enabled: Boolean = true,\n    style: GithubButtonStyle = GithubButtonStyle.Filled,\n) {\n    Button(\n        onClick = onClick,\n        modifier = modifier,\n        colors = style.colors(),\n        enabled = enabled,\n        shapes = ButtonDefaults.shapes(),\n        contentPadding =\n            if (icon != null) {\n                PaddingValues(start = 16.dp, end = 24.dp, top = 10.dp, bottom = 10.dp)\n            } else {\n                PaddingValues(horizontal = 24.dp, vertical = 10.dp)\n            },\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            icon?.invoke()\n            Text(\n                text = text,\n                style = MaterialTheme.typography.labelLarge,\n                maxLines = 1,\n                softWrap = false,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    }\n}\n\nenum class GithubButtonStyle {\n    Filled,\n    Tonal,\n    Outlined,\n    Text,\n    ;\n\n    @Composable\n    fun colors() =\n        when (this) {\n            Filled -> ButtonDefaults.buttonColors()\n            Tonal -> ButtonDefaults.filledTonalButtonColors()\n            Outlined -> ButtonDefaults.outlinedButtonColors()\n            Text -> ButtonDefaults.textButtonColors()\n        }\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt",
    "content": "package zed.rainxch.core.presentation.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.outlined.CallSplit\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.OpenInBrowser\nimport androidx.compose.material.icons.filled.Share\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material.icons.filled.Update\nimport androidx.compose.material.icons.outlined.Visibility\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.presentation.model.DiscoveryRepositoryUi\nimport zed.rainxch.core.presentation.model.GithubRepoSummaryUi\nimport zed.rainxch.core.presentation.model.GithubUserUi\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.formatReleasedAt\nimport zed.rainxch.core.presentation.utils.hasWeekNotPassed\nimport zed.rainxch.core.presentation.utils.toIcons\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.forked_repository\nimport zed.rainxch.githubstore.core.presentation.res.home_view_details\nimport zed.rainxch.githubstore.core.presentation.res.installed\nimport zed.rainxch.githubstore.core.presentation.res.open_in_browser\nimport zed.rainxch.githubstore.core.presentation.res.seen_badge\nimport zed.rainxch.githubstore.core.presentation.res.share_repository\nimport zed.rainxch.githubstore.core.presentation.res.update_available\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class)\n@Composable\nfun RepositoryCard(\n    discoveryRepositoryUi: DiscoveryRepositoryUi,\n    onClick: () -> Unit,\n    onShareClick: () -> Unit,\n    onDeveloperClick: (String) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    val uriHandler = LocalUriHandler.current\n\n    ExpressiveCard(\n        onClick = onClick,\n        modifier = modifier,\n    ) {\n        Box {\n            if (discoveryRepositoryUi.isFavourite) {\n                Icon(\n                    imageVector = Icons.Default.Favorite,\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.08f),\n                    modifier =\n                        Modifier\n                            .size(120.dp)\n                            .align(Alignment.BottomStart)\n                            .offset(x = (-32).dp, y = 32.dp),\n                )\n            }\n\n            if (discoveryRepositoryUi.isStarred) {\n                Icon(\n                    imageVector = Icons.Default.Star,\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f),\n                    modifier =\n                        Modifier\n                            .size(120.dp)\n                            .align(Alignment.TopEnd)\n                            .offset(x = 32.dp, y = (-32).dp),\n                )\n            }\n\n            Column(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(16.dp),\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                ) {\n                    Row(\n                        modifier =\n                            Modifier\n                                .clip(CircleShape)\n                                .clickable(onClick = {\n                                    onDeveloperClick(discoveryRepositoryUi.repository.owner.login)\n                                })\n                                .padding(horizontal = 4.dp, vertical = 2.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(12.dp),\n                    ) {\n                        GitHubStoreImage(\n                            imageModel = { discoveryRepositoryUi.repository.owner.avatarUrl },\n                            modifier =\n                                Modifier\n                                    .size(40.dp)\n                                    .clip(CircleShape),\n                        )\n\n                        Text(\n                            text = discoveryRepositoryUi.repository.owner.login,\n                            style = MaterialTheme.typography.titleMedium,\n                            color = MaterialTheme.colorScheme.outline,\n                            maxLines = 1,\n                            softWrap = false,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n\n                    Text(\n                        text = \"/\",\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.outline,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                        maxLines = 1,\n                        modifier = Modifier.weight(1f),\n                    )\n                }\n\n                Spacer(modifier = Modifier.height(4.dp))\n\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Text(\n                        text = discoveryRepositoryUi.repository.name,\n                        fontWeight = FontWeight.Bold,\n                        style = MaterialTheme.typography.titleLarge,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                        modifier = Modifier.weight(1f, fill = false),\n                    )\n\n                    if (discoveryRepositoryUi.repository.isFork) {\n                        ForkBadge()\n                    }\n                }\n\n                Spacer(modifier = Modifier.height(4.dp))\n\n                discoveryRepositoryUi.repository.description?.let {\n                    Text(\n                        text = it,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        maxLines = 2,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.bodyLarge,\n                        softWrap = true,\n                    )\n                }\n\n                Spacer(Modifier.height(8.dp))\n\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(16.dp),\n                ) {\n                    Text(\n                        text = \"⭐ ${discoveryRepositoryUi.repository.stargazersCount}\",\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n\n                    Text(\n                        text = \"• 🌴 ${discoveryRepositoryUi.repository.forksCount}\",\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n\n                    discoveryRepositoryUi.repository.language?.let {\n                        Text(\n                            text = \"• $it\",\n                            style = MaterialTheme.typography.titleMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            maxLines = 1,\n                            softWrap = false,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n                }\n\n                if (discoveryRepositoryUi.isInstalled || discoveryRepositoryUi.isSeen) {\n                    Spacer(Modifier.height(12.dp))\n\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        if (discoveryRepositoryUi.isInstalled) {\n                            InstallStatusBadge(\n                                isUpdateAvailable = discoveryRepositoryUi.isUpdateAvailable,\n                            )\n                        }\n\n                        if (discoveryRepositoryUi.isSeen) {\n                            SeenBadge()\n                        }\n                    }\n                }\n\n                if (discoveryRepositoryUi.repository.availablePlatforms.isNotEmpty()) {\n                    Spacer(Modifier.height(12.dp))\n\n                    FlowRow(\n                        horizontalArrangement = Arrangement.spacedBy(6.dp),\n                        verticalArrangement = Arrangement.spacedBy(6.dp),\n                    ) {\n                        discoveryRepositoryUi.repository.availablePlatforms.forEach { platform ->\n                            PlatformChip(platform = platform)\n                        }\n                    }\n                }\n\n                Spacer(Modifier.height(8.dp))\n\n                val releasedAtText =\n                    buildAnnotatedString {\n                        if (hasWeekNotPassed(discoveryRepositoryUi.repository.updatedAt)) {\n                            append(\"🔥 \")\n                        }\n\n                        append(formatReleasedAt(discoveryRepositoryUi.repository.updatedAt))\n                    }\n\n                Text(\n                    text = releasedAtText,\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.outline,\n                    maxLines = 1,\n                    softWrap = false,\n                    overflow = TextOverflow.Ellipsis,\n                )\n\n                Spacer(Modifier.height(12.dp))\n\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    GithubStoreButton(\n                        text = stringResource(Res.string.home_view_details),\n                        onClick = onClick,\n                        modifier = Modifier.weight(1f),\n                    )\n\n                    IconButton(\n                        onClick = onShareClick,\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                                contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                            ),\n                        shapes = IconButtonDefaults.shapes(),\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Share,\n                            contentDescription = stringResource(Res.string.share_repository),\n                        )\n                    }\n\n                    IconButton(\n                        onClick = {\n                            uriHandler.openUri(discoveryRepositoryUi.repository.htmlUrl)\n                        },\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                                contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                            ),\n                        shapes = IconButtonDefaults.shapes(),\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.OpenInBrowser,\n                            contentDescription = stringResource(Res.string.open_in_browser),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun PlatformChip(\n    platform: DiscoveryPlatform,\n    modifier: Modifier = Modifier,\n) {\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(16.dp),\n        color = MaterialTheme.colorScheme.surfaceContainerHighest,\n    ) {\n        FlowRow(\n            modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),\n            horizontalArrangement = Arrangement.spacedBy(2.dp),\n            verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),\n        ) {\n            platform.toIcons().forEach { icon ->\n                Icon(\n                    imageVector = icon,\n                    contentDescription = null,\n                    modifier = Modifier.size(20.dp),\n                    tint = MaterialTheme.colorScheme.onSurface,\n                )\n            }\n\n            Text(\n                text = platform.name,\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                fontWeight = FontWeight.Medium,\n                modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),\n            )\n        }\n    }\n}\n\n@Composable\nfun ForkBadge(modifier: Modifier = Modifier) {\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        color = MaterialTheme.colorScheme.secondaryContainer,\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = Icons.AutoMirrored.Outlined.CallSplit,\n                contentDescription = null,\n                modifier = Modifier.size(14.dp),\n                tint = MaterialTheme.colorScheme.onSecondaryContainer,\n            )\n            Text(\n                text = stringResource(Res.string.forked_repository),\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.onSecondaryContainer,\n                fontWeight = FontWeight.SemiBold,\n            )\n        }\n    }\n}\n\n@Composable\nfun InstallStatusBadge(\n    isUpdateAvailable: Boolean,\n    modifier: Modifier = Modifier,\n) {\n    val backgroundColor =\n        if (isUpdateAvailable) {\n            MaterialTheme.colorScheme.tertiaryContainer\n        } else {\n            MaterialTheme.colorScheme.primaryContainer\n        }\n\n    val textColor =\n        if (isUpdateAvailable) {\n            MaterialTheme.colorScheme.onTertiaryContainer\n        } else {\n            MaterialTheme.colorScheme.onPrimaryContainer\n        }\n\n    val icon =\n        if (isUpdateAvailable) {\n            Icons.Default.Update\n        } else {\n            Icons.Default.CheckCircle\n        }\n\n    val text =\n        if (isUpdateAvailable) {\n            stringResource(Res.string.update_available)\n        } else {\n            stringResource(Res.string.installed)\n        }\n\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        color = backgroundColor,\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier = Modifier.size(14.dp),\n                tint = textColor,\n            )\n            Text(\n                text = text,\n                style = MaterialTheme.typography.labelSmall,\n                color = textColor,\n                fontWeight = FontWeight.SemiBold,\n            )\n        }\n    }\n}\n\n@Composable\nfun SeenBadge(modifier: Modifier = Modifier) {\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        color = MaterialTheme.colorScheme.surfaceContainerHighest,\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = Icons.Outlined.Visibility,\n                contentDescription = null,\n                modifier = Modifier.size(14.dp),\n                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n            Text(\n                text = stringResource(Res.string.seen_badge),\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                fontWeight = FontWeight.SemiBold,\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nfun RepositoryCardPreview() {\n    GithubStoreTheme {\n        RepositoryCard(\n            discoveryRepositoryUi =\n                DiscoveryRepositoryUi(\n                    repository =\n                        GithubRepoSummaryUi(\n                            id = 0L,\n                            name = \"Hello\",\n                            fullName = \"JIFEOJEF\",\n                            owner =\n                                GithubUserUi(\n                                    id = 0L,\n                                    login = \"Skydoves\",\n                                    avatarUrl = \"ewfew\",\n                                    htmlUrl = \"grgrre\",\n                                ),\n                            description = \"Hello wolrd Hello wolrd Hello wolrd Hello wolrd Hello wolrd\",\n                            htmlUrl = \"\",\n                            stargazersCount = 20,\n                            forksCount = 4,\n                            language = \"Kotlin\",\n                            topics = null,\n                            releasesUrl = \"\",\n                            updatedAt = \"2025-12-01T12:00:00Z\",\n                            defaultBranch = \"\",\n                        ),\n                    isUpdateAvailable = true,\n                    isFavourite = true,\n                    isInstalled = true,\n                    isStarred = false,\n                ),\n            onClick = { },\n            onShareClick = { },\n            onDeveloperClick = { },\n        )\n    }\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalBottomNavigationHeight.kt",
    "content": "package zed.rainxch.core.presentation.locals\n\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.unit.Dp\n\nval LocalBottomNavigationHeight =\n    compositionLocalOf<Dp> {\n        error(\"Not initialized yet\")\n    }\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalBottomNavigationLiquid.kt",
    "content": "package zed.rainxch.core.presentation.locals\n\nimport androidx.compose.runtime.compositionLocalOf\nimport io.github.fletchmckee.liquid.LiquidState\n\nval LocalBottomNavigationLiquid =\n    compositionLocalOf<LiquidState> {\n        error(\"State not declared\")\n    }\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/DiscoveryRepositoryUi.kt",
    "content": "package zed.rainxch.core.presentation.model\n\ndata class DiscoveryRepositoryUi(\n    val isInstalled: Boolean,\n    val isUpdateAvailable: Boolean,\n    val isFavourite: Boolean,\n    val isStarred: Boolean,\n    val isSeen: Boolean = false,\n    val repository: GithubRepoSummaryUi,\n)\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt",
    "content": "package zed.rainxch.core.presentation.model\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\n\ndata class GithubRepoSummaryUi(\n    val id: Long,\n    val name: String,\n    val fullName: String,\n    val owner: GithubUserUi,\n    val description: String?,\n    val defaultBranch: String,\n    val htmlUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val language: String?,\n    val topics: ImmutableList<String>?,\n    val releasesUrl: String,\n    val updatedAt: String,\n    val isFork: Boolean = false,\n    val availablePlatforms: ImmutableList<DiscoveryPlatform> = persistentListOf(),\n)\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubUserUi.kt",
    "content": "package zed.rainxch.core.presentation.model\n\ndata class GithubUserUi(\n    val id: Long,\n    val login: String,\n    val avatarUrl: String,\n    val htmlUrl: String,\n)\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Color.kt",
    "content": "package zed.rainxch.core.presentation.theme\n\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.ui.graphics.Color\n\nval primaryLight = Color(0xFF2A638A)\nval onPrimaryLight = Color(0xFFFFFFFF)\nval primaryContainerLight = Color(0xFFCBE6FF)\nval onPrimaryContainerLight = Color(0xFF034B71)\nval secondaryLight = Color(0xFF50606F)\nval onSecondaryLight = Color(0xFFFFFFFF)\nval secondaryContainerLight = Color(0xFFD4E4F6)\nval onSecondaryContainerLight = Color(0xFF394856)\nval tertiaryLight = Color(0xFF66587B)\nval onTertiaryLight = Color(0xFFFFFFFF)\nval tertiaryContainerLight = Color(0xFFECDCFF)\nval onTertiaryContainerLight = Color(0xFF4E4162)\nval errorLight = Color(0xFFBA1A1A)\nval onErrorLight = Color(0xFFFFFFFF)\nval errorContainerLight = Color(0xFFFFDAD6)\nval onErrorContainerLight = Color(0xFF93000A)\nval backgroundLight = Color(0xFFF7F9FF)\nval onBackgroundLight = Color(0xFF181C20)\nval surfaceLight = Color(0xFFF7F9FF)\nval onSurfaceLight = Color(0xFF181C20)\nval surfaceVariantLight = Color(0xFFDEE3EA)\nval onSurfaceVariantLight = Color(0xFF42474D)\nval outlineLight = Color(0xFF72787E)\nval outlineVariantLight = Color(0xFFC1C7CE)\nval scrimLight = Color(0xFF000000)\nval inverseSurfaceLight = Color(0xFF2D3135)\nval inverseOnSurfaceLight = Color(0xFFEEF1F6)\nval inversePrimaryLight = Color(0xFF98CCF9)\nval surfaceDimLight = Color(0xFFD7DADF)\nval surfaceBrightLight = Color(0xFFF7F9FF)\nval surfaceContainerLowestLight = Color(0xFFFFFFFF)\nval surfaceContainerLowLight = Color(0xFFF1F4F9)\nval surfaceContainerLight = Color(0xFFEBEEF3)\nval surfaceContainerHighLight = Color(0xFFE6E8EE)\nval surfaceContainerHighestLight = Color(0xFFE0E3E8)\n\nval primaryDark = Color(0xFF98CCF9)\nval onPrimaryDark = Color(0xFF003350)\nval primaryContainerDark = Color(0xFF034B71)\nval onPrimaryContainerDark = Color(0xFFCBE6FF)\nval secondaryDark = Color(0xFFB8C8D9)\nval onSecondaryDark = Color(0xFF22323F)\nval secondaryContainerDark = Color(0xFF394856)\nval onSecondaryContainerDark = Color(0xFFD4E4F6)\nval tertiaryDark = Color(0xFFD1BFE7)\nval onTertiaryDark = Color(0xFF372B4A)\nval tertiaryContainerDark = Color(0xFF4E4162)\nval onTertiaryContainerDark = Color(0xFFECDCFF)\nval errorDark = Color(0xFFFFB4AB)\nval onErrorDark = Color(0xFF690005)\nval errorContainerDark = Color(0xFF93000A)\nval onErrorContainerDark = Color(0xFFFFDAD6)\nval backgroundDark = Color(0xFF101417)\nval onBackgroundDark = Color(0xFFE0E3E8)\nval surfaceDark = Color(0xFF101417)\nval onSurfaceDark = Color(0xFFE0E3E8)\nval surfaceVariantDark = Color(0xFF42474D)\nval onSurfaceVariantDark = Color(0xFFC1C7CE)\nval outlineDark = Color(0xFF8C9198)\nval outlineVariantDark = Color(0xFF42474D)\nval scrimDark = Color(0xFF000000)\nval inverseSurfaceDark = Color(0xFFE0E3E8)\nval inverseOnSurfaceDark = Color(0xFF2D3135)\nval inversePrimaryDark = Color(0xFF2A638A)\nval surfaceDimDark = Color(0xFF101417)\nval surfaceBrightDark = Color(0xFF363A3E)\nval surfaceContainerLowestDark = Color(0xFF0B0F12)\nval surfaceContainerLowDark = Color(0xFF181C20)\nval surfaceContainerDark = Color(0xFF1C2024)\nval surfaceContainerHighDark = Color(0xFF272A2E)\nval surfaceContainerHighestDark = Color(0xFF313539)\n\nfun ColorScheme.toAmoled(): ColorScheme =\n    this.copy(\n        background = Color.Black,\n        surface = Color.Black,\n        surfaceContainer = Color(0xFF0A0A0A),\n        surfaceContainerLow = Color(0xFF050505),\n        surfaceContainerLowest = Color.Black,\n        surfaceContainerHigh = Color(0xFF121212),\n        surfaceContainerHighest = Color(0xFF1A1A1A),\n        surfaceDim = Color(0xFF0D0D0D),\n        surfaceBright = Color(0xFF1F1F1F),\n        surfaceVariant = Color(0xFF121212),\n    )\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt",
    "content": "package zed.rainxch.core.presentation.theme\n\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialExpressiveTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MotionScheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.presentation.utils.darkScheme\nimport zed.rainxch.core.presentation.utils.lightScheme\n\nval oceanBlueLight =\n    lightColorScheme(\n        primary = primaryLight,\n        onPrimary = onPrimaryLight,\n        primaryContainer = primaryContainerLight,\n        onPrimaryContainer = onPrimaryContainerLight,\n        secondary = secondaryLight,\n        onSecondary = onSecondaryLight,\n        secondaryContainer = secondaryContainerLight,\n        onSecondaryContainer = onSecondaryContainerLight,\n        tertiary = tertiaryLight,\n        onTertiary = onTertiaryLight,\n        tertiaryContainer = tertiaryContainerLight,\n        onTertiaryContainer = onTertiaryContainerLight,\n        error = errorLight,\n        onError = onErrorLight,\n        errorContainer = errorContainerLight,\n        onErrorContainer = onErrorContainerLight,\n        background = backgroundLight,\n        onBackground = onBackgroundLight,\n        surface = surfaceLight,\n        onSurface = onSurfaceLight,\n        surfaceVariant = surfaceVariantLight,\n        onSurfaceVariant = onSurfaceVariantLight,\n        outline = outlineLight,\n        outlineVariant = outlineVariantLight,\n        scrim = scrimLight,\n        inverseSurface = inverseSurfaceLight,\n        inverseOnSurface = inverseOnSurfaceLight,\n        inversePrimary = inversePrimaryLight,\n        surfaceDim = surfaceDimLight,\n        surfaceBright = surfaceBrightLight,\n        surfaceContainerLowest = surfaceContainerLowestLight,\n        surfaceContainerLow = surfaceContainerLowLight,\n        surfaceContainer = surfaceContainerLight,\n        surfaceContainerHigh = surfaceContainerHighLight,\n        surfaceContainerHighest = surfaceContainerHighestLight,\n    )\n\nval oceanBlueDark =\n    darkColorScheme(\n        primary = primaryDark,\n        onPrimary = onPrimaryDark,\n        primaryContainer = primaryContainerDark,\n        onPrimaryContainer = onPrimaryContainerDark,\n        secondary = secondaryDark,\n        onSecondary = onSecondaryDark,\n        secondaryContainer = secondaryContainerDark,\n        onSecondaryContainer = onSecondaryContainerDark,\n        tertiary = tertiaryDark,\n        onTertiary = onTertiaryDark,\n        tertiaryContainer = tertiaryContainerDark,\n        onTertiaryContainer = onTertiaryContainerDark,\n        error = errorDark,\n        onError = onErrorDark,\n        errorContainer = errorContainerDark,\n        onErrorContainer = onErrorContainerDark,\n        background = backgroundDark,\n        onBackground = onBackgroundDark,\n        surface = surfaceDark,\n        onSurface = onSurfaceDark,\n        surfaceVariant = surfaceVariantDark,\n        onSurfaceVariant = onSurfaceVariantDark,\n        outline = outlineDark,\n        outlineVariant = outlineVariantDark,\n        scrim = scrimDark,\n        inverseSurface = inverseSurfaceDark,\n        inverseOnSurface = inverseOnSurfaceDark,\n        inversePrimary = inversePrimaryDark,\n        surfaceDim = surfaceDimDark,\n        surfaceBright = surfaceBrightDark,\n        surfaceContainerLowest = surfaceContainerLowestDark,\n        surfaceContainerLow = surfaceContainerLowDark,\n        surfaceContainer = surfaceContainerDark,\n        surfaceContainerHigh = surfaceContainerHighDark,\n        surfaceContainerHighest = surfaceContainerHighestDark,\n    )\n\nval deepPurpleLight =\n    lightColorScheme(\n        primary = Color(0xFF6750A4),\n        onPrimary = Color(0xFFFFFFFF),\n        primaryContainer = Color(0xFFE9DDFF),\n        onPrimaryContainer = Color(0xFF22005D),\n        secondary = Color(0xFF625B71),\n        onSecondary = Color(0xFFFFFFFF),\n        secondaryContainer = Color(0xFFE8DEF8),\n        onSecondaryContainer = Color(0xFF1E192B),\n        tertiary = Color(0xFF7E5260),\n        onTertiary = Color(0xFFFFFFFF),\n        tertiaryContainer = Color(0xFFFFD9E3),\n        onTertiaryContainer = Color(0xFF31101D),\n        error = Color(0xFFBA1A1A),\n        onError = Color(0xFFFFFFFF),\n        errorContainer = Color(0xFFFFDAD6),\n        onErrorContainer = Color(0xFF410002),\n        background = Color(0xFFFFFBFF),\n        onBackground = Color(0xFF1C1B1E),\n        surface = Color(0xFFFFFBFF),\n        onSurface = Color(0xFF1C1B1E),\n        surfaceVariant = Color(0xFFE7E0EB),\n        onSurfaceVariant = Color(0xFF49454E),\n        outline = Color(0xFF7A757F),\n        outlineVariant = Color(0xFFCAC4CF),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFF313033),\n        inverseOnSurface = Color(0xFFF4EFF4),\n        inversePrimary = Color(0xFFCFBCFF),\n        surfaceDim = Color(0xFFDED8DD),\n        surfaceBright = Color(0xFFFFFBFF),\n        surfaceContainerLowest = Color(0xFFFFFFFF),\n        surfaceContainerLow = Color(0xFFF8F2F7),\n        surfaceContainer = Color(0xFFF2ECF1),\n        surfaceContainerHigh = Color(0xFFECE6EB),\n        surfaceContainerHighest = Color(0xFFE6E1E6),\n    )\n\nval deepPurpleDark =\n    darkColorScheme(\n        primary = Color(0xFFCFBCFF),\n        onPrimary = Color(0xFF381E72),\n        primaryContainer = Color(0xFF4F378A),\n        onPrimaryContainer = Color(0xFFE9DDFF),\n        secondary = Color(0xFFCBC2DB),\n        onSecondary = Color(0xFF332D41),\n        secondaryContainer = Color(0xFF4A4458),\n        onSecondaryContainer = Color(0xFFE8DEF8),\n        tertiary = Color(0xFFEFB8C8),\n        onTertiary = Color(0xFF4A2532),\n        tertiaryContainer = Color(0xFF633B48),\n        onTertiaryContainer = Color(0xFFFFD9E3),\n        error = Color(0xFFFFB4AB),\n        onError = Color(0xFF690005),\n        errorContainer = Color(0xFF93000A),\n        onErrorContainer = Color(0xFFFFDAD6),\n        background = Color(0xFF141316),\n        onBackground = Color(0xFFE6E1E6),\n        surface = Color(0xFF141316),\n        onSurface = Color(0xFFE6E1E6),\n        surfaceVariant = Color(0xFF49454E),\n        onSurfaceVariant = Color(0xFFCAC4CF),\n        outline = Color(0xFF948F99),\n        outlineVariant = Color(0xFF49454E),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFFE6E1E6),\n        inverseOnSurface = Color(0xFF313033),\n        inversePrimary = Color(0xFF6750A4),\n        surfaceDim = Color(0xFF141316),\n        surfaceBright = Color(0xFF3A383C),\n        surfaceContainerLowest = Color(0xFF0F0E11),\n        surfaceContainerLow = Color(0xFF1C1B1E),\n        surfaceContainer = Color(0xFF201F22),\n        surfaceContainerHigh = Color(0xFF2B292D),\n        surfaceContainerHighest = Color(0xFF363438),\n    )\n\n// ============================================================================\n// FOREST GREEN THEME (Trusted & Verified)\n// ============================================================================\nval forestGreenLight =\n    lightColorScheme(\n        primary = Color(0xFF356859),\n        onPrimary = Color(0xFFFFFFFF),\n        primaryContainer = Color(0xFFB8EED9),\n        onPrimaryContainer = Color(0xFF002019),\n        secondary = Color(0xFF4C6359),\n        onSecondary = Color(0xFFFFFFFF),\n        secondaryContainer = Color(0xFFCEE9DB),\n        onSecondaryContainer = Color(0xFF092018),\n        tertiary = Color(0xFF3F6373),\n        onTertiary = Color(0xFFFFFFFF),\n        tertiaryContainer = Color(0xFFC3E8FB),\n        onTertiaryContainer = Color(0xFF001F29),\n        error = Color(0xFFBA1A1A),\n        onError = Color(0xFFFFFFFF),\n        errorContainer = Color(0xFFFFDAD6),\n        onErrorContainer = Color(0xFF410002),\n        background = Color(0xFFF5FBF7),\n        onBackground = Color(0xFF171D1A),\n        surface = Color(0xFFF5FBF7),\n        onSurface = Color(0xFF171D1A),\n        surfaceVariant = Color(0xFFDBE5DD),\n        onSurfaceVariant = Color(0xFF404943),\n        outline = Color(0xFF707973),\n        outlineVariant = Color(0xFFBFC9C1),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFF2C322F),\n        inverseOnSurface = Color(0xFFEDF2ED),\n        inversePrimary = Color(0xFF9CD1BD),\n        surfaceDim = Color(0xFFD6DBD8),\n        surfaceBright = Color(0xFFF5FBF7),\n        surfaceContainerLowest = Color(0xFFFFFFFF),\n        surfaceContainerLow = Color(0xFFF0F5F1),\n        surfaceContainer = Color(0xFFEAEFEB),\n        surfaceContainerHigh = Color(0xFFE4E9E6),\n        surfaceContainerHighest = Color(0xFFDFE4E0),\n    )\n\nval forestGreenDark =\n    darkColorScheme(\n        primary = Color(0xFF9CD1BD),\n        onPrimary = Color(0xFF00382B),\n        primaryContainer = Color(0xFF1C4F41),\n        onPrimaryContainer = Color(0xFFB8EED9),\n        secondary = Color(0xFFB2CDBF),\n        onSecondary = Color(0xFF1D352C),\n        secondaryContainer = Color(0xFF344C42),\n        onSecondaryContainer = Color(0xFFCEE9DB),\n        tertiary = Color(0xFFA7CCDE),\n        onTertiary = Color(0xFF0C3443),\n        tertiaryContainer = Color(0xFF264B5B),\n        onTertiaryContainer = Color(0xFFC3E8FB),\n        error = Color(0xFFFFB4AB),\n        onError = Color(0xFF690005),\n        errorContainer = Color(0xFF93000A),\n        onErrorContainer = Color(0xFFFFDAD6),\n        background = Color(0xFF0F1512),\n        onBackground = Color(0xFFDFE4E0),\n        surface = Color(0xFF0F1512),\n        onSurface = Color(0xFFDFE4E0),\n        surfaceVariant = Color(0xFF404943),\n        onSurfaceVariant = Color(0xFFBFC9C1),\n        outline = Color(0xFF89938C),\n        outlineVariant = Color(0xFF404943),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFFDFE4E0),\n        inverseOnSurface = Color(0xFF2C322F),\n        inversePrimary = Color(0xFF356859),\n        surfaceDim = Color(0xFF0F1512),\n        surfaceBright = Color(0xFF353B37),\n        surfaceContainerLowest = Color(0xFF0A100D),\n        surfaceContainerLow = Color(0xFF171D1A),\n        surfaceContainer = Color(0xFF1B211E),\n        surfaceContainerHigh = Color(0xFF262C28),\n        surfaceContainerHighest = Color(0xFF313733),\n    )\n\n// ============================================================================\n// SLATE GRAY THEME (Minimal Developer)\n// ============================================================================\nval slateGrayLight =\n    lightColorScheme(\n        primary = Color(0xFF535E6C),\n        onPrimary = Color(0xFFFFFFFF),\n        primaryContainer = Color(0xFFD7E3F3),\n        onPrimaryContainer = Color(0xFF101C27),\n        secondary = Color(0xFF565E6B),\n        onSecondary = Color(0xFFFFFFFF),\n        secondaryContainer = Color(0xFFDAE2F1),\n        onSecondaryContainer = Color(0xFF131C26),\n        tertiary = Color(0xFF6E5676),\n        onTertiary = Color(0xFFFFFFFF),\n        tertiaryContainer = Color(0xFFF7D9FF),\n        onTertiaryContainer = Color(0xFF281430),\n        error = Color(0xFFBA1A1A),\n        onError = Color(0xFFFFFFFF),\n        errorContainer = Color(0xFFFFDAD6),\n        onErrorContainer = Color(0xFF410002),\n        background = Color(0xFFF8F9FB),\n        onBackground = Color(0xFF191C1E),\n        surface = Color(0xFFF8F9FB),\n        onSurface = Color(0xFF191C1E),\n        surfaceVariant = Color(0xFFDFE2E9),\n        onSurfaceVariant = Color(0xFF43474E),\n        outline = Color(0xFF73777F),\n        outlineVariant = Color(0xFFC3C6CD),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFF2E3133),\n        inverseOnSurface = Color(0xFFF0F0F3),\n        inversePrimary = Color(0xFFB4C7D9),\n        surfaceDim = Color(0xFFD9D9DC),\n        surfaceBright = Color(0xFFF8F9FB),\n        surfaceContainerLowest = Color(0xFFFFFFFF),\n        surfaceContainerLow = Color(0xFFF3F3F6),\n        surfaceContainer = Color(0xFFEDEDF0),\n        surfaceContainerHigh = Color(0xFFE7E8EA),\n        surfaceContainerHighest = Color(0xFFE2E2E5),\n    )\n\nval slateGrayDark =\n    darkColorScheme(\n        primary = Color(0xFFB4C7D9),\n        onPrimary = Color(0xFF1F2F3D),\n        primaryContainer = Color(0xFF394654),\n        onPrimaryContainer = Color(0xFFD7E3F3),\n        secondary = Color(0xFFBEC6D5),\n        onSecondary = Color(0xFF28323B),\n        secondaryContainer = Color(0xFF3E4753),\n        onSecondaryContainer = Color(0xFFDAE2F1),\n        tertiary = Color(0xFFDABDE2),\n        onTertiary = Color(0xFF3E2946),\n        tertiaryContainer = Color(0xFF553F5D),\n        onTertiaryContainer = Color(0xFFF7D9FF),\n        error = Color(0xFFFFB4AB),\n        onError = Color(0xFF690005),\n        errorContainer = Color(0xFF93000A),\n        onErrorContainer = Color(0xFFFFDAD6),\n        background = Color(0xFF111416),\n        onBackground = Color(0xFFE2E2E5),\n        surface = Color(0xFF111416),\n        onSurface = Color(0xFFE2E2E5),\n        surfaceVariant = Color(0xFF43474E),\n        onSurfaceVariant = Color(0xFFC3C6CD),\n        outline = Color(0xFF8D9199),\n        outlineVariant = Color(0xFF43474E),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFFE2E2E5),\n        inverseOnSurface = Color(0xFF2E3133),\n        inversePrimary = Color(0xFF535E6C),\n        surfaceDim = Color(0xFF111416),\n        surfaceBright = Color(0xFF37393B),\n        surfaceContainerLowest = Color(0xFF0C0F11),\n        surfaceContainerLow = Color(0xFF191C1E),\n        surfaceContainer = Color(0xFF1D2022),\n        surfaceContainerHigh = Color(0xFF282A2D),\n        surfaceContainerHighest = Color(0xFF333538),\n    )\n\n// ============================================================================\n// AMBER ORANGE THEME (Energetic & Warm)\n// ============================================================================\nval amberOrangeLight =\n    lightColorScheme(\n        primary = Color(0xFF8B5000),\n        onPrimary = Color(0xFFFFFFFF),\n        primaryContainer = Color(0xFFFFDCBE),\n        onPrimaryContainer = Color(0xFF2D1600),\n        secondary = Color(0xFF715A48),\n        onSecondary = Color(0xFFFFFFFF),\n        secondaryContainer = Color(0xFFFFDCBE),\n        onSecondaryContainer = Color(0xFF28190A),\n        tertiary = Color(0xFF54643D),\n        onTertiary = Color(0xFFFFFFFF),\n        tertiaryContainer = Color(0xFFD7E9B8),\n        onTertiaryContainer = Color(0xFF131F02),\n        error = Color(0xFFBA1A1A),\n        onError = Color(0xFFFFFFFF),\n        errorContainer = Color(0xFFFFDAD6),\n        onErrorContainer = Color(0xFF410002),\n        background = Color(0xFFFFFBFF),\n        onBackground = Color(0xFF201B16),\n        surface = Color(0xFFFFFBFF),\n        onSurface = Color(0xFF201B16),\n        surfaceVariant = Color(0xFFF2DFD1),\n        onSurfaceVariant = Color(0xFF51443A),\n        outline = Color(0xFF837469),\n        outlineVariant = Color(0xFFD5C3B6),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFF36302A),\n        inverseOnSurface = Color(0xFFFAEFE7),\n        inversePrimary = Color(0xFFFFB870),\n        surfaceDim = Color(0xFFE4D9D1),\n        surfaceBright = Color(0xFFFFFBFF),\n        surfaceContainerLowest = Color(0xFFFFFFFF),\n        surfaceContainerLow = Color(0xFFFEF3EB),\n        surfaceContainer = Color(0xFFF8EDE5),\n        surfaceContainerHigh = Color(0xFFF2E7DF),\n        surfaceContainerHighest = Color(0xFFECE1DA),\n    )\n\nval amberOrangeDark =\n    darkColorScheme(\n        primary = Color(0xFFFFB870),\n        onPrimary = Color(0xFF4B2800),\n        primaryContainer = Color(0xFF6A3C00),\n        onPrimaryContainer = Color(0xFFFFDCBE),\n        secondary = Color(0xFFE2C1A3),\n        onSecondary = Color(0xFF402D1D),\n        secondaryContainer = Color(0xFF584332),\n        onSecondaryContainer = Color(0xFFFFDCBE),\n        tertiary = Color(0xFFBBCD9E),\n        onTertiary = Color(0xFF273514),\n        tertiaryContainer = Color(0xFF3D4C28),\n        onTertiaryContainer = Color(0xFFD7E9B8),\n        error = Color(0xFFFFB4AB),\n        onError = Color(0xFF690005),\n        errorContainer = Color(0xFF93000A),\n        onErrorContainer = Color(0xFFFFDAD6),\n        background = Color(0xFF18130E),\n        onBackground = Color(0xFFECE1DA),\n        surface = Color(0xFF18130E),\n        onSurface = Color(0xFFECE1DA),\n        surfaceVariant = Color(0xFF51443A),\n        onSurfaceVariant = Color(0xFFD5C3B6),\n        outline = Color(0xFF9D8E82),\n        outlineVariant = Color(0xFF51443A),\n        scrim = Color(0xFF000000),\n        inverseSurface = Color(0xFFECE1DA),\n        inverseOnSurface = Color(0xFF36302A),\n        inversePrimary = Color(0xFF8B5000),\n        surfaceDim = Color(0xFF18130E),\n        surfaceBright = Color(0xFF3F3933),\n        surfaceContainerLowest = Color(0xFF120E09),\n        surfaceContainerLow = Color(0xFF201B16),\n        surfaceContainer = Color(0xFF241F1A),\n        surfaceContainerHigh = Color(0xFF2F2A24),\n        surfaceContainerHighest = Color(0xFF3A342E),\n    )\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun GithubStoreTheme(\n    isDarkTheme: Boolean = false,\n    appTheme: AppTheme = AppTheme.OCEAN,\n    fontTheme: FontTheme = FontTheme.CUSTOM,\n    isAmoledTheme: Boolean = false,\n    content: @Composable () -> Unit,\n) {\n    val baseColorScheme =\n        when {\n            appTheme == AppTheme.DYNAMIC -> {\n                getDynamicColorScheme(isDarkTheme) ?: run {\n                    if (isDarkTheme) AppTheme.OCEAN.darkScheme else AppTheme.OCEAN.lightScheme\n                }\n            }\n\n            isDarkTheme -> {\n                appTheme.darkScheme!!\n            }\n\n            else -> {\n                appTheme.lightScheme!!\n            }\n        }\n\n    val colorScheme =\n        if (isDarkTheme && isAmoledTheme) {\n            baseColorScheme?.toAmoled()\n        } else {\n            baseColorScheme\n        }\n\n    MaterialExpressiveTheme(\n        colorScheme = colorScheme,\n        typography = getAppTypography(fontTheme),\n        motionScheme = MotionScheme.expressive(),\n        shapes = MaterialTheme.shapes,\n        content = content,\n    )\n}\n\nexpect fun isDynamicColorAvailable(): Boolean\n\n@Composable\nexpect fun getDynamicColorScheme(darkTheme: Boolean): ColorScheme?\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt",
    "content": "package zed.rainxch.core.presentation.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport org.jetbrains.compose.resources.Font\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nval jetbrainsMonoFontFamily\n    @Composable get() =\n        FontFamily(\n            Font(Res.font.jetbrains_mono_light, FontWeight.Light),\n            Font(Res.font.jetbrains_mono_regular, FontWeight.Normal),\n            Font(Res.font.jetbrains_mono_medium, FontWeight.Medium),\n            Font(Res.font.jetbrains_mono_semi_bold, FontWeight.SemiBold),\n            Font(Res.font.jetbrains_mono_bold, FontWeight.Bold),\n        )\n\nval interFontFamily\n    @Composable get() =\n        FontFamily(\n            Font(Res.font.inter_light, FontWeight.Light),\n            Font(Res.font.inter_regular, FontWeight.Normal),\n            Font(Res.font.inter_medium, FontWeight.Medium),\n            Font(Res.font.inter_semi_bold, FontWeight.SemiBold),\n            Font(Res.font.inter_bold, FontWeight.Bold),\n            Font(Res.font.inter_black, FontWeight.Black),\n        )\n\nval baseline = Typography()\n\n@Composable\nfun getAppTypography(fontTheme: FontTheme = FontTheme.CUSTOM): Typography =\n    when (fontTheme) {\n        FontTheme.SYSTEM -> {\n            baseline\n        }\n\n        FontTheme.CUSTOM -> {\n            Typography(\n                displayLarge = baseline.displayLarge.copy(fontFamily = jetbrainsMonoFontFamily),\n                displayMedium = baseline.displayMedium.copy(fontFamily = jetbrainsMonoFontFamily),\n                displaySmall = baseline.displaySmall.copy(fontFamily = jetbrainsMonoFontFamily),\n                headlineLarge = baseline.headlineLarge.copy(fontFamily = jetbrainsMonoFontFamily),\n                headlineMedium = baseline.headlineMedium.copy(fontFamily = jetbrainsMonoFontFamily),\n                headlineSmall = baseline.headlineSmall.copy(fontFamily = jetbrainsMonoFontFamily),\n                titleLarge = baseline.titleLarge.copy(fontFamily = jetbrainsMonoFontFamily),\n                titleMedium = baseline.titleMedium.copy(fontFamily = jetbrainsMonoFontFamily),\n                titleSmall = baseline.titleSmall.copy(fontFamily = jetbrainsMonoFontFamily),\n                bodyLarge = baseline.bodyLarge.copy(fontFamily = interFontFamily),\n                bodyMedium = baseline.bodyMedium.copy(fontFamily = interFontFamily),\n                bodySmall = baseline.bodySmall.copy(fontFamily = interFontFamily),\n                labelLarge = baseline.labelLarge.copy(fontFamily = interFontFamily),\n                labelMedium = baseline.labelMedium.copy(fontFamily = interFontFamily),\n                labelSmall = baseline.labelSmall.copy(fontFamily = interFontFamily),\n            )\n        }\n    }\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.AppTheme.AMBER\nimport zed.rainxch.core.domain.model.AppTheme.DYNAMIC\nimport zed.rainxch.core.domain.model.AppTheme.FOREST\nimport zed.rainxch.core.domain.model.AppTheme.OCEAN\nimport zed.rainxch.core.domain.model.AppTheme.PURPLE\nimport zed.rainxch.core.domain.model.AppTheme.SLATE\nimport zed.rainxch.core.presentation.theme.amberOrangeDark\nimport zed.rainxch.core.presentation.theme.amberOrangeLight\nimport zed.rainxch.core.presentation.theme.deepPurpleDark\nimport zed.rainxch.core.presentation.theme.deepPurpleLight\nimport zed.rainxch.core.presentation.theme.forestGreenDark\nimport zed.rainxch.core.presentation.theme.forestGreenLight\nimport zed.rainxch.core.presentation.theme.oceanBlueDark\nimport zed.rainxch.core.presentation.theme.oceanBlueLight\nimport zed.rainxch.core.presentation.theme.slateGrayDark\nimport zed.rainxch.core.presentation.theme.slateGrayLight\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nval AppTheme.lightScheme: ColorScheme?\n    get() =\n        when (this) {\n            DYNAMIC -> null\n            OCEAN -> oceanBlueLight\n            PURPLE -> deepPurpleLight\n            FOREST -> forestGreenLight\n            SLATE -> slateGrayLight\n            AMBER -> amberOrangeLight\n        }\n\nval AppTheme.darkScheme: ColorScheme?\n    get() =\n        when (this) {\n            DYNAMIC -> null\n            OCEAN -> oceanBlueDark\n            PURPLE -> deepPurpleDark\n            FOREST -> forestGreenDark\n            SLATE -> slateGrayDark\n            AMBER -> amberOrangeDark\n        }\n\nval AppTheme.primaryColor: Color?\n    get() =\n        when (this) {\n            DYNAMIC -> null\n            OCEAN -> Color(0xFF2A638A)\n            PURPLE -> Color(0xFF6750A4)\n            FOREST -> Color(0xFF356859)\n            SLATE -> Color(0xFF535E6C)\n            AMBER -> Color(0xFF8B5000)\n        }\n\nval AppTheme.displayName: String\n    @Composable\n    get() =\n        stringResource(\n            when (this) {\n                DYNAMIC -> Res.string.theme_dynamic\n                OCEAN -> Res.string.theme_ocean\n                PURPLE -> Res.string.theme_purple\n                FOREST -> Res.string.theme_forest\n                SLATE -> Res.string.theme_slate\n                AMBER -> Res.string.theme_amber\n            },\n        )\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nexpect fun ApplyAndroidSystemBars(isDarkTheme: Boolean?)\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/CountFormatter.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun formatCount(count: Int): String =\n    when {\n        count >= 1_000_000 -> stringResource(Res.string.count_millions, count / 1_000_000)\n        count >= 1000 -> stringResource(Res.string.count_thousands, count / 1000)\n        else -> count.toString()\n    }\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/DiscoveryPlatformUiResources.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun DiscoveryPlatform.toIcons(): List<ImageVector> =\n    when (this) {\n        DiscoveryPlatform.All -> {\n            listOf(\n                vectorResource(Res.drawable.ic_platform_android),\n                vectorResource(Res.drawable.ic_platform_linux),\n                vectorResource(Res.drawable.ic_platform_macos),\n                vectorResource(Res.drawable.ic_platform_windows),\n            )\n        }\n\n        DiscoveryPlatform.Android -> {\n            listOf(vectorResource(Res.drawable.ic_platform_android))\n        }\n\n        DiscoveryPlatform.Macos -> {\n            listOf(vectorResource(Res.drawable.ic_platform_macos))\n        }\n\n        DiscoveryPlatform.Windows -> {\n            listOf(vectorResource(Res.drawable.ic_platform_windows))\n        }\n\n        DiscoveryPlatform.Linux -> {\n            listOf(vectorResource(Res.drawable.ic_platform_linux))\n        }\n    }\n\n@Composable\nfun DiscoveryPlatform.toLabel(): String =\n    when (this) {\n        DiscoveryPlatform.All -> {\n            stringResource(Res.string.category_all)\n        }\n\n        DiscoveryPlatform.Android -> {\n            \"Android\"\n        }\n\n        DiscoveryPlatform.Macos -> {\n            \"macOS\"\n        }\n\n        DiscoveryPlatform.Windows -> {\n            \"Windows\"\n        }\n\n        DiscoveryPlatform.Linux -> {\n            \"Linux\"\n        }\n    }\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport kotlinx.collections.immutable.toImmutableList\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.presentation.model.GithubRepoSummaryUi\n\nfun GithubRepoSummary.toUi(): GithubRepoSummaryUi {\n    return GithubRepoSummaryUi(\n        id = id,\n        name = name,\n        fullName = fullName,\n        owner = owner.toUi(),\n        description = description,\n        defaultBranch = defaultBranch,\n        htmlUrl = htmlUrl,\n        stargazersCount = stargazersCount,\n        forksCount = forksCount,\n        language = language,\n        topics = topics?.toImmutableList(),\n        releasesUrl = releasesUrl,\n        updatedAt = updatedAt,\n        isFork = isFork,\n        availablePlatforms = availablePlatforms.toImmutableList()\n    )\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubUserMappers.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport zed.rainxch.core.domain.model.GithubUser\nimport zed.rainxch.core.presentation.model.GithubUserUi\n\nfun GithubUser.toUi(): GithubUserUi {\n    return GithubUserUi(\n        id = id,\n        login = login,\n        avatarUrl = avatarUrl,\n        htmlUrl = htmlUrl\n    )\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ObserveAsEvents.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.lifecycle.repeatOnLifecycle\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.withContext\n\n@Composable\nfun <T> ObserveAsEvents(\n    flow: Flow<T>,\n    key1: Any? = null,\n    key2: Any? = null,\n    onEvent: (T) -> Unit,\n) {\n    val lifecycleOwner = LocalLifecycleOwner.current\n    LaunchedEffect(lifecycleOwner, key1, key2) {\n        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {\n            withContext(Dispatchers.Main.immediate) {\n                flow.collect(onEvent)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.toLocalDateTime\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport kotlin.time.Clock\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\n@OptIn(ExperimentalTime::class)\nfun hasWeekNotPassed(isoInstant: String): Boolean {\n    val updated =\n        try {\n            Instant.parse(isoInstant)\n        } catch (_: IllegalArgumentException) {\n            return false\n        }\n    val now = Clock.System.now()\n    val diff = now - updated\n\n    return diff < 7.days\n}\n\n@OptIn(ExperimentalTime::class)\n@Composable\nfun formatReleasedAt(isoInstant: String): String {\n    val updated = Instant.parse(isoInstant)\n    val now = Instant.fromEpochMilliseconds(Clock.System.now().toEpochMilliseconds())\n    val diff: Duration = now - updated\n\n    val hoursDiff = diff.inWholeHours\n    val daysDiff = diff.inWholeDays\n\n    return when {\n        hoursDiff < 1 -> {\n            stringResource(Res.string.released_just_now)\n        }\n\n        hoursDiff < 24 -> {\n            stringResource(Res.string.released_hours_ago, hoursDiff)\n        }\n\n        daysDiff == 1L -> {\n            stringResource(Res.string.released_yesterday)\n        }\n\n        daysDiff < 7 -> {\n            stringResource(Res.string.released_days_ago, daysDiff)\n        }\n\n        else -> {\n            val date = updated.toLocalDateTime(TimeZone.currentSystemDefault()).date\n            stringResource(Res.string.released_on_date, date.toString())\n        }\n    }\n}\n\n@OptIn(ExperimentalTime::class)\nsuspend fun formatAddedAt(epochMillis: Long): String {\n    val updated = Instant.fromEpochMilliseconds(epochMillis)\n    val now = Clock.System.now()\n    val diff: Duration = now - updated\n\n    val hoursDiff = diff.inWholeHours\n    val daysDiff = diff.inWholeDays\n\n    return when {\n        hoursDiff < 1 -> {\n            getString(Res.string.added_just_now)\n        }\n\n        hoursDiff < 24 -> {\n            getString(Res.string.added_hours_ago, hoursDiff)\n        }\n\n        daysDiff == 1L -> {\n            getString(Res.string.added_yesterday)\n        }\n\n        daysDiff < 7 -> {\n            getString(Res.string.added_days_ago, daysDiff)\n        }\n\n        else -> {\n            val date =\n                updated\n                    .toLocalDateTime(TimeZone.currentSystemDefault())\n                    .date\n            getString(Res.string.added_on_date, date.toString())\n        }\n    }\n}\n"
  },
  {
    "path": "core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/isLiquidFrostAvailable.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nexpect fun isLiquidFrostAvailable(): Boolean\n"
  },
  {
    "path": "core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/Theme.jvm.kt",
    "content": "package zed.rainxch.core.presentation.theme\n\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.runtime.Composable\n\nactual fun isDynamicColorAvailable(): Boolean = false\n\n@Composable\nactual fun getDynamicColorScheme(darkTheme: Boolean): ColorScheme? = null\n"
  },
  {
    "path": "core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.jvm.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nactual fun ApplyAndroidSystemBars(isDarkTheme: Boolean?) {\n    // No-op\n}\n"
  },
  {
    "path": "core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/isLiquidFrostAvailable.jvm.kt",
    "content": "package zed.rainxch.core.presentation.utils\n\nactual fun isLiquidFrostAvailable(): Boolean = true\n"
  },
  {
    "path": "docs/README-BN.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ প্রকল্পের সংক্ষিপ্ত বিবরণ\n\nGitHub Store হলো GitHub রিলিজের জন্য একটি ক্রস-প্ল্যাটফর্ম অ্যাপ স্টোর, যা ওপেন-সোর্স সফটওয়্যার আবিষ্কার ও ইনস্টল করাকে সহজ করার জন্য তৈরি। এটি স্বয়ংক্রিয়ভাবে ইনস্টলযোগ্য বাইনারি (APK, EXE, DMG, AppImage, DEB, RPM) শনাক্ত করে, এক-ক্লিকে ইনস্টলেশন অফার করে, আপডেট ট্র্যাক করে এবং একটি পরিষ্কার অ্যাপ-স্টোর স্টাইলের ইন্টারফেসে রিপোজিটরির তথ্য উপস্থাপন করে।\n\nAndroid ও Desktop প্ল্যাটফর্মের জন্য Kotlin Multiplatform ও Compose Multiplatform দিয়ে তৈরি।\n\n</div>\n\n> [!CAUTION]\n> ফ্রি এবং ওপেন-সোর্স Android হুমকির মুখে। Google Android-কে একটি বন্ধ প্ল্যাটফর্মে পরিণত করবে, আপনার পছন্দের অ্যাপ ইনস্টল করার মৌলিক স্বাধীনতা সীমিত করবে। আপনার মতামত জানান – [keepandroidopen.org](https://keepandroidopen.org/)।\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 উইকি ও রিসোর্স\n\nপ্রায়শই জিজ্ঞাসিত প্রশ্ন ও দরকারী তথ্যের জন্য GitHub Store [উইকি](https://github.com/OpenHub-Store/GitHub-Store/wiki) দেখুন\n\n🌐 **ওয়েবসাইট:** [github-store.org](https://github-store.org)\n💬 **Discord:** [কমিউনিটিতে যোগ দিন](https://discord.gg/x9Cvh2Z9qS)\n📜 **গোপনীয়তা নীতি:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 আইনি নোটিশ\n\nGitHub Store একটি স্বাধীন, ওপেন-সোর্স প্রকল্প যা GitHub, Inc.-এর সাথে সম্পর্কিত নয়।  \nনামটি অ্যাপের কার্যকারিতা বর্ণনা করে (GitHub রিলিজ আবিষ্কার করা) এবং ট্রেডমার্ক মালিকানা বোঝায় না।  \nGitHub® হলো GitHub, Inc.-এর একটি নিবন্ধিত ট্রেডমার্ক।\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 ডাউনলোড\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS ব্যবহারকারীরা:** আপনি একটি সতর্কতা দেখতে পারেন যে Apple GitHub Store যাচাই করতে পারছে না। এটি ঘটে কারণ অ্যাপটি App Store-এর বাইরে বিতরণ করা হয় এবং এখনো নোটারাইজড নয়। System Settings → Privacy & Security → Open Anyway-এর মাধ্যমে এটি অনুমতি দিন।\n\n---\n\n<p align=\"center\">\n\n# 🏆 যেখানে প্রদর্শিত হয়েছে\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">২০২৬ সালের সেরা ২০টি Android অ্যাপ</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Google Play Store-এর চেয়ে ভালো ১২টি অ্যাপ স্টোর</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">বৈশিষ্ট্যযুক্ত প্রকল্প</a>\n</p>\n\n---\n\n## 🚀 বৈশিষ্ট্যসমূহ\n\n- **স্মার্ট আবিষ্কার**\n    - সময়-ভিত্তিক ফিল্টার সহ \"Trending\", \"Hot Release\" এবং \"Most Popular\" প্রকল্পের জন্য হোম বিভাগ।\n    - শুধুমাত্র বৈধ ইনস্টলযোগ্য অ্যাসেট সহ রিপোজিটরি দেখানো হয়।\n    - Android/desktop ব্যবহারকারীরা প্রথমে প্রাসঙ্গিক অ্যাপ দেখতে পান এমন প্ল্যাটফর্ম-সচেতন টপিক স্কোরিং।\n    - উন্নত প্রাসঙ্গিকতা র‍্যাঙ্কিং ও পারফরম্যান্স সহ পুনর্নির্মিত সার্চ।\n\n- **রিলিজ ব্রাউজার ও ইনস্টলেশন**\n    - শুধু সর্বশেষ নয়, যেকোনো রিলিজ থেকে ব্রাউজ ও ইনস্টল করতে রিলিজ পিকার।\n    - প্রতিটি রিপোজিটরির সব রিলিজ আনে।\n    - একক \"সর্বশেষ ইনস্টল করুন\" অ্যাকশন, এবং সব উপলব্ধ রিলিজ ও ইনস্টলারের বিস্তারযোগ্য তালিকা।\n    - স্বয়ংক্রিয় সামঞ্জস্যতা যাচাই সহ ম্যানুয়াল ইনস্টল বিকল্প।\n\n- **সমৃদ্ধ বিস্তারিত স্ক্রিন**\n    - অ্যাপের নাম, সংস্করণ ও শেয়ার অ্যাকশন।\n    - স্টার, ফর্ক, খোলা ইস্যু।\n    - রেন্ডার করা README কন্টেন্ট (\"এই অ্যাপ সম্পর্কে\")।\n    - যেকোনো নির্বাচিত রিলিজের জন্য Markdown ফরম্যাটিং সহ রিলিজ নোট।\n    - প্ল্যাটফর্ম লেবেল ও ফাইলের আকার সহ ইনস্টলারের তালিকা।\n    - ডিপ লিংকিং সাপোর্ট — URL-এর মাধ্যমে সরাসরি রিপোজিটরির বিবরণ খুলুন।\n    - একজন ডেভেলপারের রিপোজিটরি ও কার্যকলাপ অন্বেষণ করতে ডেভেলপার প্রোফাইল স্ক্রিন।\n\n- **অ্যাপ ম্যানেজমেন্ট**\n    - GitHub Store থেকে সরাসরি ইনস্টল করা অ্যাপ খুলুন, আনইনস্টল করুন এবং ডাউনগ্রেড করুন।\n    - Android: APK আর্কিটেকচার ম্যাচিং (armv7/armv8), প্যাকেজ মনিটরিং ও আপডেট ট্র্যাকিং।\n    - Desktop (Windows/macOS/Linux): ব্যবহারকারীর Downloads ফোল্ডারে ইনস্টলার ডাউনলোড করে এবং ডিফল্ট হ্যান্ডলার দিয়ে খোলে।\n\n- **স্টার করা রিপোজিটরি**\n    - অ্যাপের মধ্যে থেকে আপনার স্টার করা GitHub রিপোজিটরি সংরক্ষণ ও ব্রাউজ করুন।\n\n- **নেটওয়ার্ক ও পারফরম্যান্স**\n    - কনফিগারযোগ্য নেটওয়ার্ক রাউটিংয়ের জন্য ডায়নামিক প্রক্সি সাপোর্ট।\n    - দ্রুত লোডিং ও কমিয়ে API ব্যবহারের জন্য উন্নত ক্যাশিং সিস্টেম।\n\n---\n\n## 🔍 আমার অ্যাপ GitHub Store-এ কীভাবে দেখা যাবে?\n\nGitHub Store কোনো ব্যক্তিগত ইন্ডেক্সিং বা ম্যানুয়াল কিউরেশন নিয়ম ব্যবহার করে না।  \nনিচের শর্তগুলো পূরণ করলে আপনার প্রকল্প স্বয়ংক্রিয়ভাবে প্রদর্শিত হতে পারে:\n\n1. **GitHub-এ পাবলিক রিপোজিটরি**\n    - ভিজিবিলিটি অবশ্যই `public` হতে হবে।\n\n2. **সর্বশেষ রিলিজে ইনস্টলযোগ্য অ্যাসেট**\n    - সর্বশেষ রিলিজে সমর্থিত এক্সটেনশন সহ অন্তত একটি অ্যাসেট ফাইল থাকতে হবে:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store স্বয়ংক্রিয়ভাবে তৈরি সোর্স আর্টিফ্যাক্ট (`Source code (zip)` /\n      `Source code (tar.gz)`) উপেক্ষা করে।\n\n3. **সার্চ / টপিক দ্বারা আবিষ্কারযোগ্য**\n    - পাবলিক GitHub Search API-এর মাধ্যমে রিপোজিটরি আনা হয়।\n    - টপিক, ভাষা ও বিবরণ র‍্যাঙ্কিংয়ে সাহায্য করে:\n        - Android অ্যাপ: `android`, `mobile`, `apk`-এর মতো টপিক।\n        - Desktop অ্যাপ: `desktop`, `windows`, `linux`, `macos`, `compose-desktop`,\n          `electron`-এর মতো টপিক।\n    - অন্তত কয়েকটি স্টার থাকলে Trending/Hot Release/Most Popular বিভাগে দেখানোর সম্ভাবনা বেশি।\n\nআপনার রিপোজিটরি এই শর্তগুলো পূরণ করলে, GitHub Store সার্চের মাধ্যমে এটি খুঁজে পেতে এবং স্বয়ংক্রিয়ভাবে দেখাতে পারে — কোনো ম্যানুয়াল সাবমিশনের প্রয়োজন নেই।\n\n---\n\n## ✅ সুবিধা / কেন GitHub Store ব্যবহার করবেন?\n\n- **GitHub রিলিজে আর খোঁজাখুঁজি নয়**\n  শুধুমাত্র সেই রিপোজিটরিগুলো দেখুন যেগুলো আসলে আপনার প্ল্যাটফর্মের জন্য বাইনারি শিপ করে।\n\n- **আপনি কী ইনস্টল করেছেন তা জানে**\n  GitHub Store (Android) দিয়ে ইনস্টল করা অ্যাপ ট্র্যাক করে এবং নতুন রিলিজ পাওয়া গেলে হাইলাইট করে, যাতে আপনাকে আবার GitHub-এ খুঁজতে না হয়।\n\n- **সবসময় আপ টু ডেট**\n  ইনস্টলেশন ডিফল্টভাবে সর্বশেষ প্রকাশিত রিলিজ ব্যবহার করে, রিলিজ পিকারের মাধ্যমে যেকোনো পূর্ববর্তী রিলিজ থেকে ব্রাউজ ও ইনস্টল করার বিকল্পসহ।\n\n- **ওপেন সোর্স ও বিস্তারযোগ্য**  \n  নেটওয়ার্কিং, ডোমেইন লজিক ও UI-এর মধ্যে স্পষ্ট বিভাজন সহ KMP-তে লেখা — ফর্ক, বিস্তার বা অভিযোজন করা সহজ।\n\n---\n\n## 🔐 GitHub Store APK সাইনিং সার্টিফিকেট\n\nসব অফিসিয়াল GitHub Store রিলিজ নিচের সার্টিফিকেট ফিঙ্গারপ্রিন্ট দিয়ে স্বাক্ষরিত:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth কনফিগারেশন\n\n**সংক্ষেপে**\n1. একটি GitHub OAuth App তৈরি করুন\n2. **Client ID** কপি করুন\n3. `local.properties`-এ রাখুন\n\n<details>\n<summary><strong>সম্পূর্ণ সেটআপ গাইড দেখুন</strong></summary>\n\n  <br/>\n\n### ১ - একটি GitHub OAuth App তৈরি করুন\nএখানে যান:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| ফিল্ড                          | মান                                         |\n| ------------------------------ | ------------------------------------------- |\n| **Application name**           | যা খুশি (যেমন *GitHub Store Dev*)           |\n| **Homepage URL** | `https://github.com/username/repo_name`                   |\n| **Authorization callback URL** | `githubstore://callback`                    |\n\nতারপর **Create application**-এ ক্লিক করুন।\n\n### ২ - আপনার Client ID কপি করুন\nঅ্যাপ তৈরির পর GitHub দেখাবে:\n- **Client ID** ← এটাই আপনার দরকার\n- **Client Secret** ← ❗ এই প্রকল্পের জন্য প্রয়োজন নেই\n\n### ৩ - আপনার প্রকল্পে যোগ করুন\nআপনার প্রকল্পের `local.properties` ফাইল (প্রকল্পের রুট) খুলুন এবং যোগ করুন:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### ৪ - সিঙ্ক ও রান করুন\nপ্রকল্পটি সিঙ্ক করুন এবং অ্যাপ চালু করুন। এখন আপনি GitHub দিয়ে সাইন ইন করতে পারবেন।\n\n### ❗ গুরুত্বপূর্ণ নোট\n- `local.properties` **Git-এ কমিট হয় না**, তাই আপনার Client ID স্থানীয় থাকে।\n- এই প্রকল্পে শুধু **Client ID** দরকার (Client Secret নয়)।\n- প্রতিটি ডেভেলপারের উচিত ডেভেলপমেন্টের জন্য নিজস্ব OAuth অ্যাপ তৈরি করা।\n\n</details>\n\n---\n\n## ☕ প্রকল্পটি সাপোর্ট করুন\n\nGitHub Store একজন হাই স্কুল ছাত্র দ্বারা তৈরি ও রক্ষণাবেক্ষণ করা হয়। আপনার সাপোর্ট তাকে সাহায্য করে:\n\n✅ **অ্যাপকে বাগ-মুক্ত রাখতে** — ইস্যুতে সাড়া দিতে এবং দ্রুত ফিক্স পাঠাতে  \n✅ **কমিউনিটির অনুরোধ করা ফিচার যোগ করতে** — ব্যবহারকারীরা আসলে যা চান তা বাস্তবায়ন করতে\n\n### 💖 সাপোর্টের উপায়\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**এখন স্পনসর করতে পারছেন না?** ঠিক আছে! আপনি এভাবেও সাহায্য করতে পারেন:\n- ⭐ **এই রিপোতে স্টার দিন** — অন্যদের GitHub Store আবিষ্কার করতে সাহায্য করে\n- 🐛 **বাগ রিপোর্ট করুন** — সবার জন্য অ্যাপটি আরও ভালো করে\n- 📢 **বন্ধুদের সাথে শেয়ার করুন** — অন্য ডেভেলপার ও পরিচিতদের মধ্যে ছড়িয়ে দিন!\n- 💬 **আমাদের [Discord](https://discord.gg/x9Cvh2Z9qS)-এ যোগ দিন** — আপনার মতামত রোডম্যাপ তৈরি করে\n\nপ্রতিটি সাপোর্ট — আর্থিক হোক বা না হোক — অনেক মূল্যবান এবং এই প্রকল্পকে বাঁচিয়ে রাখে। ধন্যবাদ!\n\n---\n\n## ⚠️ দায়বদ্ধতার অস্বীকৃতি\n\nGitHub Store শুধুমাত্র GitHub-এ তৃতীয় পক্ষের ডেভেলপারদের দ্বারা ইতোমধ্যে প্রকাশিত রিলিজ অ্যাসেট আবিষ্কার ও ডাউনলোড করতে সাহায্য করে।  \nসেই ডাউনলোডগুলোর বিষয়বস্তু, নিরাপত্তা ও আচরণ সম্পূর্ণরূপে তাদের নিজস্ব লেখক ও বিতরণকারীদের দায়িত্ব, এই প্রকল্পের নয়।\n\nGitHub Store ব্যবহার করে, আপনি বুঝতে ও সম্মত হন যে আপনি নিজের ঝুঁকিতে যেকোনো ডাউনলোড করা সফটওয়্যার ইনস্টল ও চালু করছেন।  \nএই প্রকল্প কোনো ইনস্টলার নিরাপদ, ম্যালওয়্যার-মুক্ত বা কোনো নির্দিষ্ট উদ্দেশ্যের জন্য উপযুক্ত কিনা তা পর্যালোচনা, যাচাই বা নিশ্চিত করে না।\n\n---\n\n## স্টার ইতিহাস\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 লাইসেন্স\n\nGitHub Store **Apache License, Version 2.0**-এর অধীনে প্রকাশিত হবে।\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-ES.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Descripcion General del Proyecto\n\nGitHub Store es una tienda de aplicaciones multiplataforma para releases de GitHub, disenada para simplificar el descubrimiento e instalacion de software de codigo abierto. Detecta automaticamente binarios instalables (APK, EXE, DMG, AppImage, DEB, RPM), ofrece instalacion con un solo clic, rastrea actualizaciones y presenta la informacion de los repositorios en una interfaz limpia al estilo de una tienda de aplicaciones.\n\nConstruida con Kotlin Multiplatform y Compose Multiplatform para plataformas Android y Desktop.\n\n</div>\n\n> [!CAUTION]\n> Android libre y de codigo abierto esta bajo amenaza. Google convertira Android en una plataforma cerrada, restringiendo tu libertad esencial de instalar las aplicaciones que elijas. Haz que tu voz se escuche – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki y Recursos\n\nConsulta la [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) de GitHub Store para preguntas frecuentes e informacion util\n\n🌐 **Sitio web:** [github-store.org](https://github-store.org)\n💬 **Discord:** [Unete a la comunidad](https://discord.gg/x9Cvh2Z9qS)\n📜 **Politica de privacidad:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Aviso Legal\n\nGitHub Store es un proyecto independiente de codigo abierto, no afiliado a GitHub, Inc.  \nEl nombre describe la funcionalidad de la aplicacion (descubrir releases de GitHub) y no implica propiedad de marca registrada.  \nGitHub® es una marca registrada de GitHub, Inc.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 Descarga\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **Usuarios de macOS:** Es posible que veas una advertencia indicando que Apple no puede verificar GitHub Store. Esto ocurre porque la aplicacion se distribuye fuera del App Store y aun no esta notarizada. Permitela a traves de Ajustes del Sistema → Privacidad y Seguridad → Abrir de todos modos.\n\n---\n\n<p align=\"center\">\n\n# 🏆 Destacado en\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Destacado por HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">Top 20 Mejores Apps para Android 2026</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Top 12 Tiendas de Apps Mejores que Google Play Store</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Proyecto Destacado</a>\n</p>\n\n---\n\n## 🚀 Funcionalidades\n\n- **Descubrimiento inteligente**\n    - Secciones de inicio para proyectos \"Trending\", \"Hot Release\" y \"Most Popular\" con filtros basados en tiempo.\n    - Solo se muestran repositorios con archivos instalables validos.\n    - Puntuacion de temas consciente de la plataforma para que los usuarios de Android/escritorio vean las apps relevantes primero.\n    - Busqueda renovada con mejor clasificacion por relevancia y rendimiento.\n\n- **Explorador de releases e instalaciones**\n    - Selector de releases para explorar e instalar desde cualquier release, no solo la mas reciente.\n    - Obtiene todas las releases de cada repositorio.\n    - Accion unica \"Instalar ultima version\", mas una lista desplegable de todas las releases disponibles y sus instaladores.\n    - Opcion de instalacion manual con comprobaciones de compatibilidad automaticas.\n\n- **Pantalla de detalles enriquecida**\n    - Nombre de la app, version y accion de compartir.\n    - Estrellas, forks, issues abiertos.\n    - Contenido del README renderizado (\"Acerca de esta app\").\n    - Notas de la release con formato de Markdown para cualquier release seleccionada.\n    - Lista de instaladores con etiquetas de plataforma y tamanos de archivo.\n    - Soporte de enlaces profundos — abre los detalles del repositorio directamente mediante URL.\n    - Pantalla de perfil del desarrollador para explorar los repositorios y la actividad de un desarrollador.\n\n- **Gestion de aplicaciones**\n    - Abre, desinstala y degrada aplicaciones instaladas directamente desde GitHub Store.\n    - Android: coincidencia de arquitectura APK (armv7/armv8), monitoreo de paquetes y rastreo de actualizaciones.\n    - Escritorio (Windows/macOS/Linux): descarga los instaladores en la carpeta de Descargas del usuario y los abre con el manejador predeterminado.\n\n- **Repositorios destacados**\n    - Guarda y explora tus repositorios destacados de GitHub desde la aplicacion.\n\n- **Red y rendimiento**\n    - Soporte de proxy dinamico para enrutamiento de red configurable.\n    - Sistema de cache mejorado para una carga mas rapida y menor uso de la API.\n\n---\n\n## 🔍 ¿Como aparece mi aplicacion en GitHub Store?\n\nGitHub Store no utiliza ningun tipo de indexacion privada ni reglas de curacion manual.  \nTu proyecto puede aparecer automaticamente si cumple estas condiciones:\n\n1. **Repositorio publico en GitHub**\n    - La visibilidad debe ser `public`.\n\n2. **Archivos instalables en el ultimo release**\n    - El ultimo release debe contener al menos un archivo con una extension compatible:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store ignora los artefactos de codigo fuente generados automaticamente (`Source code (zip)` /\n      `Source code (tar.gz)`).\n\n3. **Descubrible mediante busqueda / topics**\n    - Los repositorios se obtienen a traves de la API publica de busqueda de GitHub.\n    - Los topics, el lenguaje y la descripcion ayudan en la clasificacion:\n        - Apps para Android: topics como `android`, `mobile`, `apk`.\n        - Apps de escritorio: topics como `desktop`, `windows`, `linux`, `macos`, `compose-desktop`,\n          `electron`.\n    - Tener al menos algunas estrellas aumenta la probabilidad de aparecer en las secciones Trending/Hot Release/Most Popular.\n\nSi tu repositorio cumple estas condiciones, GitHub Store puede encontrarlo a traves de la busqueda y mostrarlo\nautomaticamente, sin necesidad de envio manual.\n\n---\n\n## ✅ Ventajas / ¿Por que usar GitHub Store?\n\n- **No mas busquedas en los releases de GitHub**\n  Ve solo los repositorios que realmente distribuyen binarios para tu plataforma.\n\n- **Sabe lo que instalaste**\n  Rastrea las apps instaladas a traves de GitHub Store (Android) y destaca cuando hay nuevas releases disponibles, para que puedas actualizarlas sin buscar de nuevo en GitHub.\n\n- **Siempre actualizado**\n  Las instalaciones usan por defecto el ultimo release publicado, con la opcion de explorar e instalar desde\n  cualquier release anterior mediante el selector de releases.\n\n- **Codigo abierto y extensible**  \n  Escrito en KMP con una separacion clara entre red, logica de dominio e interfaz de usuario — facil de bifurcar,\n  extender o adaptar.\n\n---\n\n## 🔐 Certificado de Firma del APK de GitHub Store\n\nTodas las releases oficiales de GitHub Store estan firmadas con la siguiente huella digital del certificado:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 Configuracion de GitHub OAuth\n\n**Resumen**\n1. Crea una GitHub OAuth App\n2. Copia el **Client ID**\n3. Ponlo en `local.properties`\n\n<details>\n<summary><strong>Mostrar guia completa de configuracion</strong></summary>\n\n  <br/>\n\n### 1 - Crear una GitHub OAuth App\nVe a:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Campo                          | Valor                                       |\n| ------------------------------ | ------------------------------------------- |\n| **Application name**           | Lo que prefieras (p.ej. *GitHub Store Dev*) |\n| **Homepage URL** | `https://github.com/username/repo_name`                   |\n| **Authorization callback URL** | `githubstore://callback`                    |\n\nLuego haz clic en **Create application**.\n\n### 2 - Copiar tu Client ID\nDespues de crear la app, GitHub mostrara:\n- **Client ID** ← esto es lo que necesitas\n- **Client Secret** ← ❗ NO es necesario para este proyecto\n\n### 3 - Anadirlo a tu Proyecto\nAbre el archivo `local.properties` de tu proyecto (raiz del proyecto) y anade:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Sincronizar y Ejecutar\nSincroniza el proyecto y ejecuta la app. Ahora deberias poder iniciar sesion con GitHub.\n\n### ❗ Notas Importantes\n- `local.properties` **no se sube a Git**, por lo que tu Client ID permanece local.\n- Este proyecto solo necesita el **Client ID** (no el Client Secret).\n- Cada desarrollador deberia crear su propia OAuth app para desarrollo.\n\n</details>\n\n---\n\n## ☕ Apoya el proyecto\n\nGitHub Store esta construido y mantenido por un estudiante de instituto. Tu apoyo le ayuda a:\n\n✅ **Mantener la app libre de errores** — responder a issues y enviar correcciones rapidamente  \n✅ **Anadir funciones solicitadas por la comunidad** — implementar lo que los usuarios realmente necesitan\n\n### 💖 Formas de Apoyar\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**¿No puedes patrocinar ahora mismo?** ¡No pasa nada! Tambien puedes ayudar:\n- ⭐ **Dando una estrella a este repositorio** — ayuda a otros a descubrir GitHub Store\n- 🐛 **Reportando errores** — mejora la app para todos\n- 📢 **Compartiendolo con amigos** — difunde la palabra entre otros desarrolladores y amigos!\n- 💬 **Uniendote a nuestro [Discord](https://discord.gg/x9Cvh2Z9qS)** — tus comentarios moldean la hoja de ruta\n\nCada forma de apoyo — financiera o no — significa mucho y mantiene este proyecto vivo. ¡Gracias!\n\n---\n\n## ⚠️ Descargo de Responsabilidad\n\nGitHub Store solo te ayuda a descubrir y descargar archivos de releases que ya estan publicados en\nGitHub por desarrolladores externos.  \nLos contenidos, la seguridad y el comportamiento de esas descargas son responsabilidad exclusiva de sus\nrespectivos autores y distribuidores, no de este proyecto.\n\nAl usar GitHub Store, entiendes y aceptas que instalas y ejecutas cualquier software descargado\nbajo tu propia responsabilidad.  \nEste proyecto no revisa, valida ni garantiza que ningun instalador sea seguro, este libre de malware o\nsea adecuado para ningun proposito en particular.\n\n---\n\n## Historial de Estrellas\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 Licencia\n\nGitHub Store se distribuira bajo la **Licencia Apache, Version 2.0**.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-FR.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Aperçu du Projet\n\nGitHub Store est une boutique d'applications multiplateforme dédiée aux releases GitHub, conçue pour simplifier la découverte et l'installation de logiciels open source. Elle détecte automatiquement les binaires installables (APK, EXE, DMG, AppImage, DEB, RPM), offre une installation en un clic, suit les mises à jour et présente les informations des dépôts dans une interface épurée, façon boutique d'applications.\n\nDéveloppée avec Kotlin Multiplatform et Compose Multiplatform pour les plateformes Android et Desktop.\n\n</div>\n\n> [!CAUTION]\n> Android libre et open source est menacé. Google va transformer Android en une plateforme verrouillée, restreignant votre liberté fondamentale d'installer les applications de votre choix. Faites entendre votre voix – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki et Ressources\n\nConsultez le [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) de GitHub Store pour la FAQ et des informations utiles\n\n🌐 **Site web :** [github-store.org](https://github-store.org)\n💬 **Discord :** [Rejoindre la communauté](https://discord.gg/x9Cvh2Z9qS)\n📜 **Politique de confidentialité :** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Mentions Légales\n\nGitHub Store est un projet open source indépendant, non affilié à GitHub, Inc.  \nLe nom décrit la fonctionnalité de l'application (découverte des releases GitHub) et n'implique aucune appropriation de marque commerciale.  \nGitHub® est une marque déposée de GitHub, Inc.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 Téléchargement\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **Utilisateurs macOS :** Il est possible qu'un avertissement indique qu'Apple ne peut pas vérifier GitHub Store. Cela est dû au fait que l'application est distribuée hors de l'App Store et n'est pas encore notariée. Autorisez-la via Réglages Système → Confidentialité et sécurité → Ouvrir quand même.\n\n---\n\n<p align=\"center\">\n\n# 🏆 Mis en Avant Dans\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen :</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">Top 20 Meilleures Applications Android 2026</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Top 12 Boutiques d'Applications Meilleures que le Google Play Store</a>\n</br>\n<strong>HelloGitHub :</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Projet Mis en Avant</a>\n</p>\n\n---\n\n## 🚀 Fonctionnalités\n\n- **Découverte intelligente**\n    - Sections d'accueil pour les projets \"Trending\", \"Hot Release\" et \"Most Popular\" avec des filtres temporels.\n    - Seuls les dépôts possédant des fichiers installables valides sont affichés.\n    - Score thématique tenant compte de la plateforme pour que les utilisateurs Android/desktop voient les applications pertinentes en premier.\n    - Recherche améliorée avec un meilleur classement par pertinence et de meilleures performances.\n\n- **Navigateur de releases et installations**\n    - Sélecteur de releases pour parcourir et installer depuis n'importe quelle release, pas seulement la dernière.\n    - Récupère toutes les releases de chaque dépôt.\n    - Action unique \"Installer la dernière version\", plus une liste déroulante de toutes les releases disponibles et leurs installeurs.\n    - Option d'installation manuelle avec vérifications automatiques de compatibilité.\n\n- **Écran de détails enrichi**\n    - Nom de l'application, version et action de partage.\n    - Étoiles, forks, issues ouverts.\n    - Contenu du README rendu (\"À propos de cette application\").\n    - Notes de release avec formatage Markdown pour toute release sélectionnée.\n    - Liste des installeurs avec étiquettes de plateforme et tailles de fichiers.\n    - Prise en charge des liens profonds — ouvrez les détails d'un dépôt directement via URL.\n    - Écran de profil du développeur pour explorer ses dépôts et son activité.\n\n- **Gestion des applications**\n    - Ouvrez, désinstallez et rétrogradez les applications installées directement depuis GitHub Store.\n    - Android : correspondance d'architecture APK (armv7/armv8), surveillance des paquets et suivi des mises à jour.\n    - Desktop (Windows/macOS/Linux) : télécharge les installeurs dans le dossier Téléchargements et les ouvre avec le gestionnaire par défaut.\n\n- **Dépôts suivis**\n    - Sauvegardez et parcourez vos dépôts GitHub favoris depuis l'application.\n\n- **Réseau et performances**\n    - Prise en charge du proxy dynamique pour un routage réseau configurable.\n    - Système de cache amélioré pour un chargement plus rapide et une utilisation réduite de l'API.\n\n---\n\n## 🔍 Comment mon application apparaît-elle dans GitHub Store ?\n\nGitHub Store n'utilise aucune indexation privée ni règle de curation manuelle.  \nVotre projet peut apparaître automatiquement s'il respecte ces conditions :\n\n1. **Dépôt public sur GitHub**\n    - La visibilité doit être `public`.\n\n2. **Fichiers installables dans la dernière release**\n    - La dernière release doit contenir au moins un fichier avec une extension prise en charge :\n        - Android : `.apk`\n        - Windows : `.exe`, `.msi`\n        - macOS : `.dmg`, `.pkg`\n        - Linux : `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store ignore les archives de code source générées automatiquement (`Source code (zip)` / `Source code (tar.gz)`).\n\n3. **Découvrable via la recherche / les topics**\n    - Les dépôts sont récupérés via l'API de recherche publique de GitHub.\n    - Les topics, le langage et la description influencent le classement :\n        - Applications Android : topics comme `android`, `mobile`, `apk`.\n        - Applications desktop : topics comme `desktop`, `windows`, `linux`, `macos`, `compose-desktop`, `electron`.\n    - Avoir au moins quelques étoiles augmente la probabilité d'apparaître dans les sections Trending/Hot Release/Most Popular.\n\nSi votre dépôt remplit ces conditions, GitHub Store peut le trouver via la recherche et l'afficher automatiquement — aucune soumission manuelle n'est requise.\n\n---\n\n## ✅ Avantages / Pourquoi utiliser GitHub Store ?\n\n- **Fini de fouiller dans les releases GitHub**\n  Ne voyez que les dépôts qui distribuent réellement des binaires pour votre plateforme.\n\n- **Sait ce que vous avez installé**\n  Suit les applications installées via GitHub Store (Android) et signale les nouvelles releases disponibles, pour que vous puissiez les mettre à jour sans retourner chercher sur GitHub.\n\n- **Toujours à jour**\n  Les installations utilisent par défaut la dernière release publiée, avec la possibilité de parcourir et d'installer depuis n'importe quelle release précédente via le sélecteur de releases.\n\n- **Open source et extensible**  \n  Écrit en KMP avec une séparation claire entre le réseau, la logique métier et l'interface — facile à forker, étendre ou adapter.\n\n---\n\n## 🔐 Certificat de Signature APK de GitHub Store\n\nToutes les releases officielles de GitHub Store sont signées avec l'empreinte de certificat suivante :\n\nSHA-256 :\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 Configuration de GitHub OAuth\n\n**En résumé**\n1. Créez une GitHub OAuth App\n2. Copiez le **Client ID**\n3. Placez-le dans `local.properties`\n\n<details>\n<summary><strong>Afficher le guide de configuration complet</strong></summary>\n\n  <br/>\n\n### 1 - Créer une GitHub OAuth App\nRendez-vous sur :\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Champ                          | Valeur                                        |\n| ------------------------------ | --------------------------------------------- |\n| **Application name**           | Ce que vous voulez (ex. *GitHub Store Dev*)   |\n| **Homepage URL**               | `https://github.com/username/repo_name`       |\n| **Authorization callback URL** | `githubstore://callback`                      |\n\nPuis cliquez sur **Create application**.\n\n### 2 - Copier votre Client ID\nAprès la création, GitHub affichera :\n- **Client ID** ← c'est ce dont vous avez besoin\n- **Client Secret** ← ❗ NON requis pour ce projet\n\n### 3 - L'ajouter à votre projet\nOuvrez le fichier `local.properties` de votre projet (à la racine) et ajoutez :\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Synchroniser et lancer\nSynchronisez le projet et lancez l'application. Vous devriez maintenant pouvoir vous connecter avec GitHub.\n\n### ❗ Remarques importantes\n- `local.properties` **n'est pas versionné dans Git**, votre Client ID reste donc local.\n- Ce projet n'a besoin que du **Client ID** (pas du Client Secret).\n- Chaque développeur devrait créer sa propre OAuth App pour le développement.\n\n</details>\n\n---\n\n## ☕ Soutenir le projet\n\nGitHub Store est développé et maintenu par un lycéen. Votre soutien l'aide à :\n\n✅ **Garder l'application sans bugs** — répondre aux issues et publier des correctifs rapidement  \n✅ **Ajouter des fonctionnalités demandées par la communauté** — implémenter ce dont les utilisateurs ont vraiment besoin\n\n### 💖 Façons de Soutenir\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**Vous ne pouvez pas sponsoriser maintenant ?** Pas de problème ! Vous pouvez tout de même aider en :\n- ⭐ **Mettant une étoile à ce dépôt** — aide les autres à découvrir GitHub Store\n- 🐛 **Signalant des bugs** — améliore l'application pour tout le monde\n- 📢 **Partageant avec vos amis** — faites passer le mot à d'autres développeurs et proches !\n- 💬 **Rejoignant notre [Discord](https://discord.gg/x9Cvh2Z9qS)** — vos retours façonnent la feuille de route\n\nChaque forme de soutien — financière ou non — compte énormément et maintient ce projet en vie. Merci !\n\n---\n\n## ⚠️ Avertissement\n\nGitHub Store vous aide uniquement à découvrir et télécharger des fichiers de releases déjà publiés sur GitHub par des développeurs tiers.  \nLe contenu, la sécurité et le comportement de ces téléchargements relèvent de la seule responsabilité de leurs auteurs et distributeurs respectifs, et non de ce projet.\n\nEn utilisant GitHub Store, vous comprenez et acceptez que vous installez et exécutez tout logiciel téléchargé à vos propres risques.  \nCe projet ne révise, ne valide ni ne garantit qu'un installeur est sûr, exempt de logiciels malveillants ou adapté à un usage particulier.\n\n---\n\n## Historique des Étoiles\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 Licence\n\nGitHub Store sera publié sous la **Licence Apache, Version 2.0**.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-HI.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ परियोजना का अवलोकन\n\nGitHub Store एक क्रॉस-प्लेटफ़ॉर्म ऐप स्टोर है जो GitHub रिलीज़ के लिए बनाया गया है। यह ओपन-सोर्स सॉफ़्टवेयर खोजने और इंस्टॉल करने को आसान बनाता है। यह इंस्टॉल करने योग्य बायनरी फ़ाइलों (APK, EXE, DMG, AppImage, DEB, RPM) को स्वचालित रूप से पहचानता है, वन-क्लिक इंस्टॉलेशन प्रदान करता है, अपडेट को ट्रैक करता है और रिपॉजिटरी की जानकारी को एक साफ ऐप-स्टोर शैली के इंटरफ़ेस में प्रस्तुत करता है।\n\nAndroid और Desktop प्लेटफ़ॉर्म के लिए Kotlin Multiplatform और Compose Multiplatform के साथ बनाया गया है।\n\n</div>\n\n> [!CAUTION]\n> मुक्त और ओपन-सोर्स Android खतरे में है। Google, Android को एक बंद प्लेटफ़ॉर्म में बदल देगा, जो आपकी पसंद के ऐप्स इंस्टॉल करने की आपकी मूलभूत स्वतंत्रता को प्रतिबंधित करेगा। अपनी आवाज़ उठाएं – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki और संसाधन\n\nFAQ और उपयोगी जानकारी के लिए GitHub Store [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) देखें\n\n🌐 **वेबसाइट:** [github-store.org](https://github-store.org)\n💬 **Discord:** [समुदाय से जुड़ें](https://discord.gg/x9Cvh2Z9qS)\n📜 **गोपनीयता नीति:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 कानूनी नोटिस\n\nGitHub Store एक स्वतंत्र ओपन-सोर्स प्रोजेक्ट है जो GitHub, Inc. से संबद्ध नहीं है।  \nयह नाम ऐप की कार्यक्षमता (GitHub रिलीज़ खोजना) का वर्णन करता है और इसका अर्थ ट्रेडमार्क स्वामित्व नहीं है।  \nGitHub® GitHub, Inc. का एक पंजीकृत ट्रेडमार्क है।\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 डाउनलोड\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS उपयोगकर्ता:** आपको एक चेतावनी दिख सकती है कि Apple, GitHub Store को सत्यापित नहीं कर सकता। ऐसा इसलिए होता है क्योंकि ऐप App Store के बाहर वितरित की जाती है और अभी तक notarized नहीं है। System Settings → Privacy & Security → Open Anyway के ज़रिए इसे अनुमति दें।\n\n---\n\n<p align=\"center\">\n\n# 🏆 मीडिया में शामिल\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">2026 के शीर्ष 20 सर्वश्रेष्ठ Android ऐप्स</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Google Play Store से बेहतर शीर्ष 12 ऐप स्टोर</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">विशेष रूप से प्रदर्शित प्रोजेक्ट</a>\n</p>\n\n---\n\n## 🚀 सुविधाएँ\n\n- **स्मार्ट खोज**\n    - समय-आधारित फ़िल्टर के साथ \"Trending\", \"Hot Release\" और \"Most Popular\" प्रोजेक्ट के होम सेक्शन।\n    - केवल वे रिपॉजिटरी दिखाई जाती हैं जिनमें वैध इंस्टॉल योग्य फ़ाइलें हों।\n    - प्लेटफ़ॉर्म-जागरूक टॉपिक स्कोरिंग ताकि Android/डेस्कटॉप उपयोगकर्ताओं को पहले प्रासंगिक ऐप्स दिखें।\n    - बेहतर प्रासंगिकता रैंकिंग और प्रदर्शन के साथ नई सर्च।\n\n- **रिलीज़ ब्राउज़र और इंस्टॉलेशन**\n    - रिलीज़ सेलेक्टर जो किसी भी रिलीज़ से ब्राउज़ और इंस्टॉल करने की सुविधा देता है, न कि केवल नवीनतम से।\n    - प्रत्येक रिपॉजिटरी की सभी रिलीज़ लाता है।\n    - एकल \"नवीनतम संस्करण इंस्टॉल करें\" क्रिया, साथ ही सभी उपलब्ध रिलीज़ और उनके इंस्टॉलर की विस्तार योग्य सूची।\n    - स्वचालित संगतता जांच के साथ मैन्युअल इंस्टॉलेशन विकल्प।\n\n- **समृद्ध विवरण स्क्रीन**\n    - ऐप का नाम, संस्करण और शेयर करने की क्रिया।\n    - स्टार, फ़ोर्क, खुले इश्यू।\n    - रेंडर किया गया README कंटेंट (\"इस ऐप के बारे में\")।\n    - किसी भी चुनी हुई रिलीज़ के लिए Markdown फ़ॉर्मेटिंग के साथ रिलीज़ नोट्स।\n    - प्लेटफ़ॉर्म लेबल और फ़ाइल आकारों के साथ इंस्टॉलर की सूची।\n    - डीप लिंकिंग सपोर्ट — URL के ज़रिए सीधे रिपॉजिटरी विवरण खोलें।\n    - किसी डेवलपर के रिपॉजिटरी और गतिविधि को एक्सप्लोर करने के लिए डेवलपर प्रोफ़ाइल स्क्रीन।\n\n- **ऐप प्रबंधन**\n    - इंस्टॉल किए गए ऐप्स को GitHub Store से सीधे खोलें, अनइंस्टॉल करें और डाउनग्रेड करें।\n    - Android: APK आर्किटेक्चर मिलान (armv7/armv8), पैकेज मॉनिटरिंग और अपडेट ट्रैकिंग।\n    - Desktop (Windows/macOS/Linux): इंस्टॉलर को उपयोगकर्ता के Downloads फ़ोल्डर में डाउनलोड करता है और डिफ़ॉल्ट हैंडलर से खोलता है।\n\n- **स्टार की गई रिपॉजिटरी**\n    - ऐप के भीतर से अपनी GitHub स्टार की हुई रिपॉजिटरी सेव करें और ब्राउज़ करें।\n\n- **नेटवर्क और प्रदर्शन**\n    - कॉन्फ़िगर करने योग्य नेटवर्क रूटिंग के लिए डायनामिक प्रॉक्सी सपोर्ट।\n    - तेज़ लोडिंग और कम API उपयोग के लिए उन्नत कैशिंग सिस्टम।\n\n---\n\n## 🔍 मेरी ऐप GitHub Store में कैसे दिखेगी?\n\nGitHub Store किसी भी निजी इंडेक्सिंग या मैन्युअल क्यूरेशन नियमों का उपयोग नहीं करता।  \nआपका प्रोजेक्ट स्वचालित रूप से दिख सकता है यदि यह इन शर्तों को पूरा करता है:\n\n1. **GitHub पर सार्वजनिक रिपॉजिटरी**\n    - विज़िबिलिटी `public` होनी चाहिए।\n\n2. **नवीनतम रिलीज़ में इंस्टॉल योग्य फ़ाइलें**\n    - नवीनतम रिलीज़ में समर्थित एक्सटेंशन वाली कम से कम एक फ़ाइल होनी चाहिए:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store GitHub के स्वचालित रूप से जनरेट किए गए सोर्स आर्काइव (`Source code (zip)` / `Source code (tar.gz)`) को अनदेखा करता है।\n\n3. **सर्च / टॉपिक द्वारा खोजे जाने योग्य**\n    - रिपॉजिटरी GitHub की सार्वजनिक सर्च API के माध्यम से लाई जाती हैं।\n    - टॉपिक, भाषा और विवरण रैंकिंग में मदद करते हैं:\n        - Android ऐप्स: `android`, `mobile`, `apk` जैसे टॉपिक।\n        - Desktop ऐप्स: `desktop`, `windows`, `linux`, `macos`, `compose-desktop`, `electron` जैसे टॉपिक।\n    - कम से कम कुछ स्टार होने से Trending/Hot Release/Most Popular सेक्शन में दिखने की संभावना बढ़ जाती है।\n\nयदि आपकी रिपॉजिटरी इन शर्तों को पूरा करती है, तो GitHub Store इसे सर्च के माध्यम से खोज सकता है और स्वचालित रूप से दिखा सकता है — किसी मैन्युअल सबमिशन की आवश्यकता नहीं।\n\n---\n\n## ✅ फ़ायदे / GitHub Store क्यों उपयोग करें?\n\n- **GitHub रिलीज़ में हाथ से खोजना बंद**\n  केवल वे रिपॉजिटरी देखें जो आपके प्लेटफ़ॉर्म के लिए वास्तव में बायनरी शिप करती हैं।\n\n- **जानता है कि आपने क्या इंस्टॉल किया है**\n  GitHub Store (Android) के माध्यम से इंस्टॉल किए गए ऐप्स को ट्रैक करता है और जब नई रिलीज़ उपलब्ध होती हैं तो हाइलाइट करता है, ताकि आप GitHub पर दोबारा खोजे बिना उन्हें अपडेट कर सकें।\n\n- **हमेशा अप-टू-डेट**\n  इंस्टॉलेशन डिफ़ॉल्ट रूप से नवीनतम प्रकाशित रिलीज़ का उपयोग करती है, रिलीज़ सेलेक्टर के माध्यम से किसी भी पिछली रिलीज़ से ब्राउज़ और इंस्टॉल करने के विकल्प के साथ।\n\n- **ओपन सोर्स और एक्सटेंसिबल**  \n  नेटवर्किंग, डोमेन लॉजिक और UI के बीच स्पष्ट अलगाव के साथ KMP में लिखा गया — फ़ोर्क करना, एक्सटेंड करना या अडैप्ट करना आसान।\n\n---\n\n## 🔐 GitHub Store APK साइनिंग सर्टिफ़िकेट\n\nसभी आधिकारिक GitHub Store रिलीज़ निम्नलिखित सर्टिफ़िकेट फ़िंगरप्रिंट के साथ साइन की जाती हैं:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth कॉन्फ़िगरेशन\n\n**संक्षेप में**\n1. GitHub OAuth App बनाएं\n2. **Client ID** कॉपी करें\n3. इसे `local.properties` में डालें\n\n<details>\n<summary><strong>पूरा सेटअप गाइड दिखाएं</strong></summary>\n\n  <br/>\n\n### 1 - GitHub OAuth App बनाएं\nयहाँ जाएं:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| फ़ील्ड                         | मान                                             |\n| ------------------------------ | ----------------------------------------------- |\n| **Application name**           | कुछ भी (जैसे *GitHub Store Dev*)               |\n| **Homepage URL**               | `https://github.com/username/repo_name`         |\n| **Authorization callback URL** | `githubstore://callback`                        |\n\nफिर **Create application** पर क्लिक करें।\n\n### 2 - अपना Client ID कॉपी करें\nऐप बनाने के बाद, GitHub दिखाएगा:\n- **Client ID** ← आपको यही चाहिए\n- **Client Secret** ← ❗ इस प्रोजेक्ट के लिए आवश्यक नहीं\n\n### 3 - अपने प्रोजेक्ट में जोड़ें\nअपने प्रोजेक्ट की `local.properties` फ़ाइल (प्रोजेक्ट की रूट) खोलें और जोड़ें:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Sync करें और चलाएं\nप्रोजेक्ट को सिंक करें और ऐप चलाएं। अब आप GitHub से साइन इन कर सकेंगे।\n\n### ❗ महत्वपूर्ण नोट्स\n- `local.properties` **Git में कमिट नहीं होता**, इसलिए आपका Client ID लोकल रहता है।\n- इस प्रोजेक्ट को केवल **Client ID** की ज़रूरत है (Client Secret की नहीं)।\n- प्रत्येक डेवलपर को विकास के लिए अपना खुद का OAuth App बनाना चाहिए।\n\n</details>\n\n---\n\n## ☕ प्रोजेक्ट को सपोर्ट करें\n\nGitHub Store एक हाई स्कूल छात्र द्वारा बनाया और मेंटेन किया जाता है। आपका सपोर्ट उन्हें इसमें मदद करता है:\n\n✅ **ऐप को बग-मुक्त रखना** — इश्यू का जवाब देना और जल्दी फ़िक्स शिप करना  \n✅ **कम्युनिटी द्वारा अनुरोधित सुविधाएं जोड़ना** — वह लागू करना जो उपयोगकर्ताओं को वास्तव में चाहिए\n\n### 💖 सपोर्ट के तरीके\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**अभी स्पॉन्सर नहीं कर सकते?** कोई बात नहीं! आप फिर भी इन तरीकों से मदद कर सकते हैं:\n- ⭐ **इस रिपॉजिटरी को स्टार करें** — दूसरों को GitHub Store खोजने में मदद करता है\n- 🐛 **बग रिपोर्ट करें** — ऐप को सभी के लिए बेहतर बनाता है\n- 📢 **दोस्तों के साथ शेयर करें** — अन्य डेवलपर्स और दोस्तों को बताएं!\n- 💬 **हमारे [Discord](https://discord.gg/x9Cvh2Z9qS) से जुड़ें** — आपकी प्रतिक्रिया रोडमैप को आकार देती है\n\nहर प्रकार का सपोर्ट — वित्तीय हो या नहीं — बहुत मायने रखता है और इस प्रोजेक्ट को ज़िंदा रखता है। धन्यवाद!\n\n---\n\n## ⚠️ अस्वीकरण\n\nGitHub Store केवल तृतीय-पक्ष डेवलपर्स द्वारा GitHub पर पहले से प्रकाशित रिलीज़ फ़ाइलों को खोजने और डाउनलोड करने में आपकी मदद करता है।  \nउन डाउनलोड की सामग्री, सुरक्षा और व्यवहार पूरी तरह से उनके संबंधित लेखकों और वितरकों की ज़िम्मेदारी है, इस प्रोजेक्ट की नहीं।\n\nGitHub Store का उपयोग करके, आप समझते और स्वीकार करते हैं कि आप किसी भी डाउनलोड किए गए सॉफ़्टवेयर को अपने जोखिम पर इंस्टॉल और चलाते हैं।  \nयह प्रोजेक्ट किसी भी इंस्टॉलर की समीक्षा, सत्यापन या गारंटी नहीं करता कि वह सुरक्षित है, मैलवेयर-मुक्त है, या किसी विशेष उद्देश्य के लिए उपयुक्त है।\n\n---\n\n## Star इतिहास\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 लाइसेंस\n\nGitHub Store **Apache License, Version 2.0** के तहत रिलीज़ किया जाएगा।\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-IT.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Panoramica del Progetto\n\nGitHub Store è uno store di applicazioni multipiattaforma per le release di GitHub, progettato per semplificare la scoperta e l'installazione di software open source. Rileva automaticamente i binari installabili (APK, EXE, DMG, AppImage, DEB, RPM), offre l'installazione con un solo clic, traccia gli aggiornamenti e presenta le informazioni sui repository in un'interfaccia pulita in stile app store.\n\nSviluppato con Kotlin Multiplatform e Compose Multiplatform per le piattaforme Android e Desktop.\n\n</div>\n\n> [!CAUTION]\n> Android libero e open source è sotto minaccia. Google trasformerà Android in una piattaforma chiusa, limitando la tua libertà fondamentale di installare le app che preferisci. Fai sentire la tua voce – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki e Risorse\n\nConsulta la [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) di GitHub Store per le FAQ e informazioni utili\n\n🌐 **Sito web:** [github-store.org](https://github-store.org)\n💬 **Discord:** [Unisciti alla community](https://discord.gg/x9Cvh2Z9qS)\n📜 **Informativa sulla privacy:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Note Legali\n\nGitHub Store è un progetto open source indipendente, non affiliato a GitHub, Inc.  \nIl nome descrive la funzionalità dell'app (scoperta delle release di GitHub) e non implica alcuna proprietà di marchio.  \nGitHub® è un marchio registrato di GitHub, Inc.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 Download\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **Utenti macOS:** Potresti vedere un avviso che indica che Apple non riesce a verificare GitHub Store. Ciò accade perché l'app è distribuita al di fuori dell'App Store e non è ancora notarizzata. Consentila tramite Impostazioni di Sistema → Privacy e Sicurezza → Apri comunque.\n\n---\n\n<p align=\"center\">\n\n# 🏆 In Evidenza Su\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">Top 20 Migliori App Android 2026</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Top 12 Store di App Migliori del Google Play Store</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Progetto in Evidenza</a>\n</p>\n\n---\n\n## 🚀 Funzionalità\n\n- **Scoperta intelligente**\n    - Sezioni nella home per i progetti \"Trending\", \"Hot Release\" e \"Most Popular\" con filtri temporali.\n    - Vengono mostrati solo i repository con file installabili validi.\n    - Punteggio dei topic consapevole della piattaforma, così gli utenti Android/desktop vedono prima le app rilevanti.\n    - Ricerca rinnovata con classificazione per pertinenza e prestazioni migliorate.\n\n- **Browser delle release e installazioni**\n    - Selettore di release per sfogliare e installare da qualsiasi release, non solo l'ultima.\n    - Recupera tutte le release di ogni repository.\n    - Azione unica \"Installa l'ultima versione\", più un elenco espandibile di tutte le release disponibili e i loro installer.\n    - Opzione di installazione manuale con controlli automatici di compatibilità.\n\n- **Schermata dettagli ricca**\n    - Nome dell'app, versione e azione di condivisione.\n    - Stelle, fork, issue aperte.\n    - Contenuto del README renderizzato (\"Informazioni su questa app\").\n    - Note di release con formattazione Markdown per qualsiasi release selezionata.\n    - Elenco degli installer con etichette di piattaforma e dimensioni dei file.\n    - Supporto ai deep link — apri i dettagli di un repository direttamente tramite URL.\n    - Schermata del profilo sviluppatore per esplorare i repository e l'attività di uno sviluppatore.\n\n- **Gestione delle applicazioni**\n    - Apri, disinstalla e declassa le app installate direttamente da GitHub Store.\n    - Android: corrispondenza dell'architettura APK (armv7/armv8), monitoraggio dei pacchetti e tracciamento degli aggiornamenti.\n    - Desktop (Windows/macOS/Linux): scarica gli installer nella cartella Download dell'utente e li apre con il gestore predefinito.\n\n- **Repository preferiti**\n    - Salva e sfoglia i tuoi repository GitHub preferiti dall'app.\n\n- **Rete e prestazioni**\n    - Supporto proxy dinamico per il routing di rete configurabile.\n    - Sistema di cache migliorato per un caricamento più rapido e un minor utilizzo dell'API.\n\n---\n\n## 🔍 Come appare la mia app su GitHub Store?\n\nGitHub Store non utilizza alcun sistema di indicizzazione privato né regole di curazione manuale.  \nIl tuo progetto può apparire automaticamente se rispetta queste condizioni:\n\n1. **Repository pubblico su GitHub**\n    - La visibilità deve essere `public`.\n\n2. **File installabili nell'ultima release**\n    - L'ultima release deve contenere almeno un file con un'estensione supportata:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store ignora gli archivi del codice sorgente generati automaticamente (`Source code (zip)` / `Source code (tar.gz)`).\n\n3. **Scopribile tramite ricerca / topic**\n    - I repository vengono recuperati tramite l'API di ricerca pubblica di GitHub.\n    - I topic, il linguaggio e la descrizione influenzano il ranking:\n        - App Android: topic come `android`, `mobile`, `apk`.\n        - App desktop: topic come `desktop`, `windows`, `linux`, `macos`, `compose-desktop`, `electron`.\n    - Avere almeno alcune stelle aumenta la probabilità di apparire nelle sezioni Trending/Hot Release/Most Popular.\n\nSe il tuo repository soddisfa queste condizioni, GitHub Store può trovarlo tramite la ricerca e mostrarlo automaticamente — nessuna submission manuale richiesta.\n\n---\n\n## ✅ Vantaggi / Perché usare GitHub Store?\n\n- **Niente più ricerche tra le release di GitHub**\n  Vedi solo i repository che distribuiscono effettivamente binari per la tua piattaforma.\n\n- **Sa cosa hai installato**\n  Traccia le app installate tramite GitHub Store (Android) e segnala quando sono disponibili nuove release, così puoi aggiornarle senza dover tornare a cercare su GitHub.\n\n- **Sempre aggiornato**\n  Le installazioni utilizzano per impostazione predefinita l'ultima release pubblicata, con la possibilità di sfogliare e installare qualsiasi release precedente tramite il selettore di release.\n\n- **Open source ed estensibile**  \n  Scritto in KMP con una netta separazione tra rete, logica di dominio e UI — facile da forkare, estendere o adattare.\n\n---\n\n## 🔐 Certificato di Firma APK di GitHub Store\n\nTutte le release ufficiali di GitHub Store sono firmate con la seguente impronta digitale del certificato:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 Configurazione di GitHub OAuth\n\n**In sintesi**\n1. Crea una GitHub OAuth App\n2. Copia il **Client ID**\n3. Inseriscilo in `local.properties`\n\n<details>\n<summary><strong>Mostra la guida completa alla configurazione</strong></summary>\n\n  <br/>\n\n### 1 - Creare una GitHub OAuth App\nVai su:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Campo                          | Valore                                          |\n| ------------------------------ | ----------------------------------------------- |\n| **Application name**           | Quello che preferisci (es. *GitHub Store Dev*)  |\n| **Homepage URL**               | `https://github.com/username/repo_name`         |\n| **Authorization callback URL** | `githubstore://callback`                        |\n\nPoi clicca su **Create application**.\n\n### 2 - Copiare il Client ID\nDopo la creazione, GitHub mostrerà:\n- **Client ID** ← questo è ciò di cui hai bisogno\n- **Client Secret** ← ❗ NON richiesto per questo progetto\n\n### 3 - Aggiungerlo al progetto\nApri il file `local.properties` del tuo progetto (nella root) e aggiungi:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Sincronizzare ed eseguire\nSincronizza il progetto ed esegui l'app. Ora dovresti poter accedere con GitHub.\n\n### ❗ Note importanti\n- `local.properties` **non è incluso in Git**, quindi il tuo Client ID rimane locale.\n- Questo progetto ha bisogno solo del **Client ID** (non del Client Secret).\n- Ogni sviluppatore dovrebbe creare la propria OAuth App per lo sviluppo.\n\n</details>\n\n---\n\n## ☕ Supporta il progetto\n\nGitHub Store è sviluppato e mantenuto da uno studente liceale. Il tuo supporto lo aiuta a:\n\n✅ **Mantenere l'app priva di bug** — rispondere alle issue e pubblicare correzioni rapidamente  \n✅ **Aggiungere funzionalità richieste dalla community** — implementare ciò di cui gli utenti hanno davvero bisogno\n\n### 💖 Come Supportare\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**Non puoi sponsorizzare in questo momento?** Nessun problema! Puoi comunque aiutare:\n- ⭐ **Mettendo una stella a questo repository** — aiuta gli altri a scoprire GitHub Store\n- 🐛 **Segnalando bug** — migliora l'app per tutti\n- 📢 **Condividendo con gli amici** — spargi la voce tra altri sviluppatori e amici!\n- 💬 **Unendoti al nostro [Discord](https://discord.gg/x9Cvh2Z9qS)** — il tuo feedback plasma la roadmap\n\nOgni forma di supporto — finanziario o meno — significa moltissimo e mantiene vivo questo progetto. Grazie!\n\n---\n\n## ⚠️ Dichiarazione di Non Responsabilità\n\nGitHub Store ti aiuta soltanto a scoprire e scaricare file di release già pubblicati su GitHub da sviluppatori terzi.  \nIl contenuto, la sicurezza e il comportamento di questi download sono di esclusiva responsabilità dei rispettivi autori e distributori, non di questo progetto.\n\nUsando GitHub Store, capisci e accetti che installi ed esegui qualsiasi software scaricato a tuo rischio e pericolo.  \nQuesto progetto non revisiona, valida né garantisce che un installer sia sicuro, privo di malware o adatto a uno scopo specifico.\n\n---\n\n## Cronologia delle Stelle\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 Licenza\n\nGitHub Store sarà rilasciato sotto la **Licenza Apache, Versione 2.0**.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-JA.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ プロジェクト概要\n\nGitHub Store は、オープンソースソフトウェアの発見とインストールを簡単にするために設計された、GitHub リリース向けのクロスプラットフォームアプリストアです。インストール可能なバイナリ（APK、EXE、DMG、AppImage、DEB、RPM）を自動的に検出し、ワンクリックインストール、アップデート追跡、そしてアプリストア風のクリーンなインターフェースでリポジトリ情報を提供します。\n\nAndroid および Desktop プラットフォーム向けに Kotlin Multiplatform と Compose Multiplatform で構築されています。\n\n</div>\n\n> [!CAUTION]\n> 自由でオープンソースな Android が危機に瀕しています。Google は Android を閉鎖的なプラットフォームに変え、好きなアプリをインストールするというあなたの基本的な自由を制限しようとしています。声を上げてください – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki とリソース\n\nFAQ や役立つ情報は GitHub Store の [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) をご覧ください\n\n🌐 **ウェブサイト:** [github-store.org](https://github-store.org)\n💬 **Discord:** [コミュニティに参加する](https://discord.gg/x9Cvh2Z9qS)\n📜 **プライバシーポリシー:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 法的事項\n\nGitHub Store は GitHub, Inc. と無関係の独立したオープンソースプロジェクトです。  \nこの名称はアプリの機能（GitHub リリースの発見）を説明するものであり、商標の所有権を意味するものではありません。  \nGitHub® は GitHub, Inc. の登録商標です。\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 ダウンロード\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS ユーザーの方へ:** Apple が GitHub Store を検証できないという警告が表示される場合があります。これはアプリが App Store 外で配布されており、まだ公証されていないためです。システム設定 → プライバシーとセキュリティ → このまま開く から許可してください。\n\n---\n\n<p align=\"center\">\n\n# 🏆 掲載メディア\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">2026年 おすすめAndroidアプリ TOP 20</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Google Play ストアより優れたアプリストア TOP 12</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">注目プロジェクト</a>\n</p>\n\n---\n\n## 🚀 機能\n\n- **スマートな発見**\n    - 時間ベースのフィルター付きで「Trending」「Hot Release」「Most Popular」プロジェクトのホームセクション。\n    - 有効なインストール可能ファイルを持つリポジトリのみが表示されます。\n    - Android/デスクトップユーザーが関連アプリを最初に見られるよう、プラットフォームを考慮したトピックスコアリング。\n    - 関連性ランキングとパフォーマンスが向上した刷新された検索。\n\n- **リリースブラウザとインストール**\n    - 最新版だけでなく、任意のリリースから閲覧・インストールできるリリースセレクター。\n    - 各リポジトリのすべてのリリースを取得。\n    - 「最新版をインストール」ワンアクション、および利用可能なすべてのリリースとインストーラーの展開可能なリスト。\n    - 自動互換性チェック付きの手動インストールオプション。\n\n- **充実した詳細画面**\n    - アプリ名、バージョン、共有アクション。\n    - スター数、フォーク数、オープンイシュー数。\n    - レンダリングされた README コンテンツ（「このアプリについて」）。\n    - 選択した任意のリリースの Markdown 形式リリースノート。\n    - プラットフォームラベルとファイルサイズ付きのインストーラーリスト。\n    - ディープリンクサポート — URL からリポジトリ詳細を直接開く。\n    - 開発者のリポジトリと活動を探索するための開発者プロフィール画面。\n\n- **アプリ管理**\n    - インストール済みアプリを GitHub Store から直接開く、アンインストール、ダウングレード。\n    - Android：APK アーキテクチャマッチング（armv7/armv8）、パッケージ監視、アップデート追跡。\n    - Desktop（Windows/macOS/Linux）：インストーラーをユーザーのダウンロードフォルダーにダウンロードし、デフォルトハンドラーで開く。\n\n- **スター付きリポジトリ**\n    - アプリ内から GitHub のスター付きリポジトリを保存・閲覧。\n\n- **ネットワークとパフォーマンス**\n    - 設定可能なネットワークルーティングのための動的プロキシサポート。\n    - より高速な読み込みと API 使用量削減のための強化されたキャッシュシステム。\n\n---\n\n## 🔍 自分のアプリを GitHub Store に表示させるには？\n\nGitHub Store はプライベートインデックスや手動キュレーションルールを一切使用していません。  \n以下の条件を満たせば、プロジェクトは自動的に表示される可能性があります：\n\n1. **GitHub 上の公開リポジトリ**\n    - 可視性は `public` に設定されている必要があります。\n\n2. **最新リリースにインストール可能なファイル**\n    - 最新リリースに、サポートされている拡張子のファイルが少なくとも 1 つ含まれている必要があります：\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store は GitHub が自動生成するソースコードアーカイブ（`Source code (zip)` / `Source code (tar.gz)`）を無視します。\n\n3. **検索 / トピックスによる発見可能性**\n    - リポジトリは GitHub の公開検索 API 経由で取得されます。\n    - トピックス、言語、説明がランキングに影響します：\n        - Android アプリ：`android`、`mobile`、`apk` などのトピックス。\n        - デスクトップアプリ：`desktop`、`windows`、`linux`、`macos`、`compose-desktop`、`electron` などのトピックス。\n    - スターが少しでもあると、Trending/Hot Release/Most Popular セクションに表示される可能性が高まります。\n\nリポジトリがこれらの条件を満たしている場合、GitHub Store は検索を通じてそれを見つけ、自動的に表示できます — 手動での申請は不要です。\n\n---\n\n## ✅ メリット / GitHub Store を使う理由\n\n- **GitHub リリースを手動で探す手間がなくなる**\n  あなたのプラットフォーム向けにバイナリを実際に提供しているリポジトリだけを表示。\n\n- **インストール済みアプリを把握している**\n  GitHub Store 経由でインストールされたアプリ（Android）を追跡し、新しいリリースが利用可能になると通知。GitHub で再度検索することなく更新できます。\n\n- **常に最新**\n  インストールはデフォルトで最新公開リリースを使用。リリースセレクターを通じて以前のリリースも閲覧・インストール可能。\n\n- **オープンソースで拡張可能**  \n  ネットワーク、ドメインロジック、UI が明確に分離された KMP で記述 — フォーク、拡張、カスタマイズが簡単。\n\n---\n\n## 🔐 GitHub Store APK 署名証明書\n\nすべての公式 GitHub Store リリースは以下の証明書フィンガープリントで署名されています：\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth の設定\n\n**要約**\n1. GitHub OAuth App を作成する\n2. **Client ID** をコピーする\n3. `local.properties` に記入する\n\n<details>\n<summary><strong>完全なセットアップガイドを表示</strong></summary>\n\n  <br/>\n\n### 1 - GitHub OAuth App の作成\n以下に移動：\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| フィールド                     | 値                                              |\n| ------------------------------ | ----------------------------------------------- |\n| **Application name**           | 任意の名前（例：*GitHub Store Dev*）            |\n| **Homepage URL**               | `https://github.com/username/repo_name`         |\n| **Authorization callback URL** | `githubstore://callback`                        |\n\n**Create application** をクリックします。\n\n### 2 - Client ID のコピー\n作成後、GitHub は以下を表示します：\n- **Client ID** ← これが必要なもの\n- **Client Secret** ← ❗ このプロジェクトでは不要\n\n### 3 - プロジェクトへの追加\nプロジェクトの `local.properties` ファイル（プロジェクトのルート）を開き、以下を追加：\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - 同期と実行\nプロジェクトを同期してアプリを実行します。これで GitHub でサインインできるようになります。\n\n### ❗ 重要な注意事項\n- `local.properties` は **Git にコミットされない** ため、Client ID はローカルに留まります。\n- このプロジェクトに必要なのは **Client ID** のみです（Client Secret は不要）。\n- 各開発者は開発用に独自の OAuth App を作成する必要があります。\n\n</details>\n\n---\n\n## ☕ プロジェクトを支援する\n\nGitHub Store は高校生によって開発・メンテナンスされています。あなたのサポートが以下を可能にします：\n\n✅ **アプリをバグのない状態に保つ** — イシューに対応し、修正を迅速にリリースする  \n✅ **コミュニティからのリクエスト機能を追加する** — ユーザーが本当に必要とするものを実装する\n\n### 💖 支援方法\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**今すぐスポンサーできない場合は？** 大丈夫！以下の方法でも支援できます：\n- ⭐ **このリポジトリにスターを付ける** — 他の人が GitHub Store を発見するのに役立ちます\n- 🐛 **バグを報告する** — アプリをすべての人のために改善します\n- 📢 **友人にシェアする** — 他の開発者や友人に広めてください！\n- 💬 **[Discord](https://discord.gg/x9Cvh2Z9qS) に参加する** — あなたのフィードバックがロードマップを形成します\n\n金銭的か否かを問わず、あらゆる形のサポートが大きな意味を持ち、このプロジェクトを生き続けさせます。ありがとうございます！\n\n---\n\n## ⚠️ 免責事項\n\nGitHub Store は、サードパーティの開発者によって GitHub に既に公開されているリリースファイルの発見とダウンロードを支援するだけです。  \nこれらのダウンロードのコンテンツ、安全性、動作はそれぞれの作者と配布者の責任であり、このプロジェクトとは無関係です。\n\nGitHub Store を使用することで、ダウンロードしたソフトウェアのインストールと実行は自己責任で行うことを理解し同意したものとみなされます。  \nこのプロジェクトは、いかなるインストーラーが安全であること、マルウェアを含まないこと、または特定の目的に適していることを審査、検証、保証するものではありません。\n\n---\n\n## スター履歴\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 ライセンス\n\nGitHub Store は **Apache License, Version 2.0** の下でリリースされます。\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-KR.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ 프로젝트 개요\n\nGitHub Store는 오픈소스 소프트웨어의 발견과 설치를 간편하게 만들기 위해 설계된 GitHub 릴리즈 전용 크로스플랫폼 앱 스토어입니다. 설치 가능한 바이너리(APK, EXE, DMG, AppImage, DEB, RPM)를 자동으로 감지하고, 원클릭 설치를 제공하며, 업데이트를 추적하고, 리포지터리 정보를 깔끔한 앱 스토어 스타일 인터페이스로 제공합니다.\n\nAndroid 및 Desktop 플랫폼을 위해 Kotlin Multiplatform과 Compose Multiplatform으로 개발되었습니다.\n\n</div>\n\n> [!CAUTION]\n> 자유롭고 오픈소스인 Android가 위협받고 있습니다. Google은 Android를 폐쇄적인 플랫폼으로 바꿔 원하는 앱을 설치할 수 있는 기본적인 자유를 제한하려 합니다. 목소리를 높여주세요 – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 위키 및 리소스\n\nFAQ와 유용한 정보는 GitHub Store [위키](https://github.com/OpenHub-Store/GitHub-Store/wiki)를 확인하세요\n\n🌐 **웹사이트:** [github-store.org](https://github-store.org)\n💬 **Discord:** [커뮤니티 참여하기](https://discord.gg/x9Cvh2Z9qS)\n📜 **개인정보처리방침:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 법적 고지\n\nGitHub Store는 GitHub, Inc.와 무관한 독립적인 오픈소스 프로젝트입니다.  \n이 이름은 앱의 기능(GitHub 릴리즈 발견)을 설명하는 것으로, 상표권 소유를 주장하는 것이 아닙니다.  \nGitHub®는 GitHub, Inc.의 등록 상표입니다.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 다운로드\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS 사용자:** Apple이 GitHub Store를 확인할 수 없다는 경고가 표시될 수 있습니다. 이는 앱이 App Store 외부에서 배포되고 아직 공증되지 않았기 때문입니다. 시스템 설정 → 개인 정보 보호 및 보안 → 그래도 열기를 통해 허용하세요.\n\n---\n\n<p align=\"center\">\n\n# 🏆 미디어 소개\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">2026년 최고의 Android 앱 TOP 20</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Google Play 스토어보다 나은 앱 스토어 TOP 12</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">추천 프로젝트</a>\n</p>\n\n---\n\n## 🚀 기능\n\n- **스마트 발견**\n    - 시간 기반 필터가 있는 \"Trending\", \"Hot Release\", \"Most Popular\" 프로젝트 홈 섹션.\n    - 유효한 설치 가능 파일이 있는 리포지터리만 표시됩니다.\n    - Android/데스크톱 사용자가 관련 앱을 먼저 볼 수 있도록 플랫폼을 고려한 토픽 점수 산정.\n    - 향상된 관련성 순위와 성능을 갖춘 새로워진 검색.\n\n- **릴리즈 브라우저 및 설치**\n    - 최신 버전뿐만 아니라 모든 릴리즈에서 탐색하고 설치할 수 있는 릴리즈 선택기.\n    - 각 리포지터리의 모든 릴리즈를 가져옵니다.\n    - 단일 \"최신 버전 설치\" 액션과 사용 가능한 모든 릴리즈 및 설치 프로그램의 확장 가능한 목록.\n    - 자동 호환성 확인이 있는 수동 설치 옵션.\n\n- **풍부한 상세 화면**\n    - 앱 이름, 버전, 공유 액션.\n    - 스타 수, 포크 수, 열린 이슈.\n    - 렌더링된 README 콘텐츠(\"이 앱 소개\").\n    - 선택한 릴리즈의 Markdown 형식 릴리즈 노트.\n    - 플랫폼 레이블과 파일 크기가 있는 설치 프로그램 목록.\n    - 딥 링크 지원 — URL을 통해 리포지터리 상세 정보를 직접 열기.\n    - 개발자의 리포지터리와 활동을 탐색하는 개발자 프로필 화면.\n\n- **앱 관리**\n    - GitHub Store에서 직접 설치된 앱을 열고, 제거하고, 다운그레이드.\n    - Android: APK 아키텍처 매칭(armv7/armv8), 패키지 모니터링, 업데이트 추적.\n    - Desktop(Windows/macOS/Linux): 설치 프로그램을 사용자의 다운로드 폴더에 다운로드하고 기본 핸들러로 열기.\n\n- **즐겨찾기 리포지터리**\n    - 앱 내에서 GitHub 즐겨찾기 리포지터리를 저장하고 탐색.\n\n- **네트워크 및 성능**\n    - 설정 가능한 네트워크 라우팅을 위한 동적 프록시 지원.\n    - 더 빠른 로딩과 API 사용량 감소를 위한 향상된 캐싱 시스템.\n\n---\n\n## 🔍 내 앱이 GitHub Store에 표시되려면?\n\nGitHub Store는 비공개 인덱싱이나 수동 큐레이션 규칙을 전혀 사용하지 않습니다.  \n다음 조건을 충족하면 프로젝트가 자동으로 표시될 수 있습니다:\n\n1. **GitHub의 공개 리포지터리**\n    - 가시성이 `public`으로 설정되어야 합니다.\n\n2. **최신 릴리즈의 설치 가능한 파일**\n    - 최신 릴리즈에 지원되는 확장자를 가진 파일이 하나 이상 포함되어야 합니다:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store는 GitHub의 자동 생성 소스 코드 아카이브(`Source code (zip)` / `Source code (tar.gz)`)를 무시합니다.\n\n3. **검색 / 토픽을 통한 발견 가능성**\n    - 리포지터리는 GitHub의 공개 검색 API를 통해 가져옵니다.\n    - 토픽, 언어, 설명이 순위에 영향을 줍니다:\n        - Android 앱: `android`, `mobile`, `apk` 같은 토픽.\n        - 데스크톱 앱: `desktop`, `windows`, `linux`, `macos`, `compose-desktop`, `electron` 같은 토픽.\n    - 스타가 몇 개라도 있으면 Trending/Hot Release/Most Popular 섹션에 표시될 가능성이 높아집니다.\n\n리포지터리가 이 조건을 충족하면 GitHub Store가 검색을 통해 자동으로 찾아 표시할 수 있습니다 — 수동 제출이 필요 없습니다.\n\n---\n\n## ✅ 장점 / GitHub Store를 사용해야 하는 이유\n\n- **GitHub 릴리즈를 일일이 찾아다닐 필요 없음**\n  내 플랫폼용 바이너리를 실제로 제공하는 리포지터리만 확인하세요.\n\n- **설치된 앱을 파악하고 있음**\n  GitHub Store를 통해 설치된 앱(Android)을 추적하고 새 릴리즈가 나오면 알려줘서 GitHub에서 다시 검색하지 않아도 업데이트할 수 있습니다.\n\n- **항상 최신 상태**\n  설치는 기본적으로 가장 최근에 게시된 릴리즈를 사용하며, 릴리즈 선택기를 통해 이전 릴리즈도 탐색·설치할 수 있습니다.\n\n- **오픈소스이며 확장 가능**  \n  네트워크, 도메인 로직, UI 사이에 명확한 분리가 있는 KMP로 작성 — 포크, 확장, 적용이 쉽습니다.\n\n---\n\n## 🔐 GitHub Store APK 서명 인증서\n\n모든 공식 GitHub Store 릴리즈는 다음 인증서 지문으로 서명되어 있습니다:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth 설정\n\n**요약**\n1. GitHub OAuth App 생성\n2. **Client ID** 복사\n3. `local.properties`에 입력\n\n<details>\n<summary><strong>전체 설정 가이드 보기</strong></summary>\n\n  <br/>\n\n### 1 - GitHub OAuth App 생성\n다음으로 이동:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| 필드                           | 값                                              |\n| ------------------------------ | ----------------------------------------------- |\n| **Application name**           | 원하는 이름(예: *GitHub Store Dev*)             |\n| **Homepage URL**               | `https://github.com/username/repo_name`         |\n| **Authorization callback URL** | `githubstore://callback`                        |\n\n그런 다음 **Create application**을 클릭합니다.\n\n### 2 - Client ID 복사\n앱 생성 후 GitHub는 다음을 표시합니다:\n- **Client ID** ← 필요한 것\n- **Client Secret** ← ❗ 이 프로젝트에는 필요 없음\n\n### 3 - 프로젝트에 추가\n프로젝트의 `local.properties` 파일(프로젝트 루트)을 열고 다음을 추가:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - 동기화 및 실행\n프로젝트를 동기화하고 앱을 실행합니다. 이제 GitHub로 로그인할 수 있습니다.\n\n### ❗ 중요 사항\n- `local.properties`는 **Git에 커밋되지 않아** Client ID가 로컬에 유지됩니다.\n- 이 프로젝트는 **Client ID**만 필요합니다(Client Secret 불필요).\n- 각 개발자는 개발을 위해 자체 OAuth App을 만들어야 합니다.\n\n</details>\n\n---\n\n## ☕ 프로젝트 지원하기\n\nGitHub Store는 고등학생이 개발하고 유지관리하고 있습니다. 여러분의 지원이 그에게 다음을 가능하게 합니다:\n\n✅ **앱을 버그 없이 유지** — 이슈에 대응하고 수정 사항을 빠르게 배포  \n✅ **커뮤니티 요청 기능 추가** — 사용자가 실제로 필요로 하는 것을 구현\n\n### 💖 지원 방법\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**지금 후원하기 어렵다면?** 괜찮습니다! 다음과 같은 방법으로도 도울 수 있습니다:\n- ⭐ **이 리포지터리에 스타 달기** — 다른 사람들이 GitHub Store를 발견하는 데 도움이 됩니다\n- 🐛 **버그 신고** — 모든 사람을 위한 앱 개선에 기여합니다\n- 📢 **친구에게 공유** — 다른 개발자와 지인들에게 알려주세요!\n- 💬 **[Discord](https://discord.gg/x9Cvh2Z9qS) 참여** — 여러분의 피드백이 로드맵을 형성합니다\n\n금전적이든 아니든 모든 형태의 지원이 큰 의미를 가지며 이 프로젝트를 살아있게 합니다. 감사합니다!\n\n---\n\n## ⚠️ 면책 조항\n\nGitHub Store는 서드파티 개발자가 GitHub에 이미 게시한 릴리즈 파일을 발견하고 다운로드하는 것을 돕는 역할만 합니다.  \n해당 다운로드의 내용, 안전성, 동작은 이 프로젝트가 아닌 각 제작자 및 배포자의 책임입니다.\n\nGitHub Store를 사용함으로써 다운로드한 소프트웨어의 설치 및 실행이 전적으로 자신의 책임임을 이해하고 동의합니다.  \n이 프로젝트는 어떠한 설치 프로그램이 안전하거나, 악성 코드가 없거나, 특정 목적에 적합하다는 것을 검토, 검증 또는 보장하지 않습니다.\n\n---\n\n## 스타 히스토리\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 라이선스\n\nGitHub Store는 **Apache License, Version 2.0** 하에 출시됩니다.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-PL.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../composeApp/src/commonMain/composeResources/drawable/app-icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/></a>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/></a>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/></a>\n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/></a>\n  </br>\n  </br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n\n<p align=\"center\">\n  <a href=\"https://trendshift.io/repositories/15655\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/15655\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"55\" />\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<p align=\"center\">\n<a href=\"/README.md\">English</a> | <a href=\"/docs/README-ES.md\">Español</a> | <a href=\"/docs/README-FR.md\">Français</a> | <a href=\"/docs/README-IT.md\">Italiano</a> | <a href=\"/docs/README-RU.md\">Русский</a> | <a href=\"/docs/README-PL.md\"><b>Polski</b></a> | <a href=\"/docs/README-TR.md\">Türkçe</a> | <a href=\"/docs/README-ZH.md\">中文</a> | <a href=\"/docs/README-JA.md\">日本語</a> | <a href=\"/docs/README-KR.md\">한국어</a> | <a href=\"/docs/README-BN.md\">বাংলা</a> | <a href=\"/docs/README-HI.md\">हिन्दी</a>\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Przegląd projektu\n\nGitHub Store to wieloplatformowy sklep z aplikacjami dla wydań z GitHuba, zaprojektowany w celu uproszczenia odkrywania i instalowania oprogramowania open source. Automatycznie wykrywa instalowalne pliki binarne (APK, EXE, DMG, AppImage, DEB, RPM), oferuje instalację jednym kliknięciem, śledzi aktualizacje i prezentuje informacje o repozytoriach w przejrzystym interfejsie w stylu sklepu z aplikacjami.\n\nZbudowany z użyciem Kotlin Multiplatform i Compose Multiplatform na platformy Android i Desktop.\n\n</div>\n\n> [!CAUTION]\n> Wolny i otwartoźródłowy Android jest zagrożony. Google zamieni Androida w zamkniętą platformę, ograniczając Twoją podstawową wolność instalowania wybranych aplikacji. Wyraź swoje zdanie – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki i zasoby\n\nSprawdź [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) GitHub Store, aby znaleźć odpowiedzi na najczęściej zadawane pytania i przydatne informacje\n\n🌐 **Strona internetowa:** [github-store.org](https://github-store.org)\n💬 **Discord:** [Dołącz do społeczności](https://discord.gg/x9Cvh2Z9qS)\n📜 **Polityka prywatności:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Informacja prawna\n\nGitHub Store jest niezależnym projektem open source, niezwiązanym z GitHub, Inc.\nNazwa opisuje funkcjonalność aplikacji (odkrywanie wydań z GitHuba) i nie sugeruje własności znaku towarowego.\nGitHub® jest zarejestrowanym znakiem towarowym GitHub, Inc.\n\n</div>\n\n---\n\n<div align=\"center\">\n\n# 🔃 Pobierz\n\n</div>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"80\"/>\n</a>\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://discord.gg/x9Cvh2Z9qS\">\n  <img src=\"https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white\" alt=\"Join Discord\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **Użytkownicy macOS:** Możesz zobaczyć ostrzeżenie, że Apple nie może zweryfikować GitHub Store. Dzieje się tak, ponieważ aplikacja jest dystrybuowana poza App Store i nie jest jeszcze notaryzowana. Zezwól na nią w Ustawienia systemowe → Prywatność i bezpieczeństwo → Otwórz mimo to.\n\n---\n\n<div align=\"center\">\n\n# 🏆 Wyróżniony w\n\n</div>\n\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Wyróżniony przez HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">Top 20 Najlepszych Aplikacji na Androida 2026</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Top 12 Sklepów z Aplikacjami Lepszych niż Google Play Store</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Wyróżniony Projekt</a>\n</p>\n\n---\n\n## 🚀 Funkcje\n\n- **Inteligentne odkrywanie**\n    - Sekcje na stronie głównej dla projektów \"Trending\", \"Hot Release\" i \"Most Popular\" z filtrami czasowymi.\n    - Wyświetlane są tylko repozytoria z poprawnymi plikami instalacyjnymi.\n    - Ocena tematów uwzględniająca platformę, aby użytkownicy Androida/komputera widzieli najpierw odpowiednie aplikacje.\n    - Odnowione wyszukiwanie z lepszym rankingiem trafności i wydajnością.\n\n- **Przeglądarka wydań i instalacja**\n    - Selektor wydań umożliwiający przeglądanie i instalację z dowolnego wydania, nie tylko najnowszego.\n    - Pobiera wszystkie wydania każdego repozytorium.\n    - Pojedyncza akcja \"Zainstaluj najnowszą wersję\" oraz rozwijana lista wszystkich dostępnych wydań i ich instalatorów.\n    - Opcja ręcznej instalacji z automatycznym sprawdzaniem kompatybilności.\n\n- **Szczegółowy ekran informacji**\n    - Nazwa aplikacji, wersja, przycisk \"Zainstaluj najnowszą wersję\" oraz akcja udostępniania.\n    - Gwiazdki, forki, otwarte zgłoszenia.\n    - Wyrenderowana zawartość README (\"O tej aplikacji\").\n    - Notatki do wydania w formacie Markdown dla dowolnego wybranego wydania.\n    - Lista instalatorów z etykietami platform i rozmiarami plików.\n    - Obsługa głębokich linków — otwiera szczegóły repozytorium bezpośrednio przez URL.\n    - Ekran profilu dewelopera do przeglądania repozytoriów i aktywności dewelopera.\n\n- **Zarządzanie aplikacjami**\n    - Otwieraj, odinstalowuj i instaluj starsze wersje aplikacji bezpośrednio z GitHub Store.\n    - Android: dopasowywanie architektury APK (armv7/armv8), monitorowanie pakietów i śledzenie aktualizacji.\n    - Komputer (Windows/macOS/Linux): pobiera instalatory do folderu Pobrane użytkownika i otwiera je domyślnym programem obsługi.\n\n- **Ulubione repozytoria**\n    - Zapisuj i przeglądaj swoje ulubione repozytoria z GitHuba bezpośrednio w aplikacji.\n\n- **Sieć i wydajność**\n    - Dynamiczna obsługa proxy do konfigurowalnego routingu sieciowego.\n    - Ulepszony system pamięci podręcznej zapewniający szybsze ładowanie i mniejsze zużycie API.\n\n- **Wieloplatformowy UX**\n    - Android: natywny ekran powitalny, obsługa wygasania sesji i adaptacyjna ikona.\n    - Komputer: priorytetowa obsługa AppImage na Linuxie wraz z formatami DEB i RPM.\n    - Zlokalizowany w 12 językach: angielski, hiszpański, francuski, japoński, koreański, polski, rosyjski, chiński, bengalski, hindi, włoski i turecki.\n\n---\n\n## 🔍 Jak moja aplikacja pojawia się w GitHub Store?\n\nGitHub Store nie korzysta z żadnego prywatnego indeksowania ani ręcznych zasad kuracji.\nTwój projekt może pojawić się automatycznie, jeśli spełnia następujące warunki:\n\n1. **Publiczne repozytorium na GitHubie**\n    - Widoczność musi być ustawiona na `public`.\n\n2. **Pliki instalacyjne w najnowszym wydaniu**\n    - Najnowsze wydanie musi zawierać co najmniej jeden plik z obsługiwanym rozszerzeniem:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store ignoruje automatycznie generowane archiwa kodu źródłowego (`Source code (zip)` /\n      `Source code (tar.gz)`).\n\n3. **Wykrywalność przez wyszukiwanie / tematy**\n    - Repozytoria są pobierane za pośrednictwem publicznego API wyszukiwania GitHuba.\n    - Tematy, język i opis pomagają w klasyfikacji:\n        - Aplikacje na Androida: tematy takie jak `android`, `mobile`, `apk`.\n        - Aplikacje desktopowe: tematy takie jak `desktop`, `windows`, `linux`, `macos`, `compose-desktop`,\n          `electron`.\n    - Posiadanie przynajmniej kilku gwiazdek zwiększa prawdopodobieństwo pojawienia się w sekcjach Trending/Hot Release/Most Popular.\n\nJeśli Twoje repozytorium spełnia te warunki, GitHub Store może je znaleźć przez wyszukiwanie i wyświetlić\nautomatycznie, bez konieczności ręcznego zgłaszania.\n\n---\n\n## 🧭 Jak działa GitHub Store (przegląd)\n\n1. **Wyszukiwanie**\n    - Używa endpointu `/search/repositories` GitHuba z zapytaniami dostosowanymi do platformy.\n    - Stosuje prostą ocenę opartą na tematach, języku i opisie.\n    - Filtruje zarchiwizowane repozytoria oraz te z małą liczbą sygnałów.\n\n2. **Sprawdzanie wydań i plików**\n    - Dla repozytoriów-kandydatów wywołuje `/repos/{owner}/{repo}/releases/latest`.\n    - Sprawdza tablicę `assets` pod kątem rozszerzeń plików specyficznych dla platformy.\n    - Jeśli nie znaleziono odpowiedniego pliku, repozytorium jest wykluczane z wyników.\n    - Użytkownicy mogą również przeglądać wszystkie wydania za pomocą selektora wydań.\n\n3. **Ekran szczegółów**\n    - Informacje o repozytorium: nazwa, właściciel, opis, gwiazdki, forki, zgłoszenia.\n    - Przeglądarka wydań: nawiguj po dowolnym wydaniu z jego tagiem, datą, changelogiem i plikami.\n    - README: ładowany z głównej gałęzi i renderowany jako \"O tej aplikacji\".\n    - Link do profilu dewelopera i akcja udostępniania.\n    - Dostępny przez głębokie linki do bezpośredniej nawigacji.\n\n4. **Proces instalacji**\n    - Gdy użytkownik kliknie \"Zainstaluj najnowszą wersję\" lub wybierze konkretne wydanie:\n        - Wybiera najbardziej odpowiedni plik dla bieżącej platformy (z dopasowaniem architektury na Androidzie).\n        - Przesyła strumieniowo pobieranie z obsługą pamięci podręcznej.\n        - Deleguje do instalatora systemu operacyjnego (instalator APK na Androidzie, domyślny program obsługi na komputerze).\n        - Na Androidzie rejestruje instalację w lokalnej bazie danych i używa monitorowania pakietów, aby utrzymać listę zainstalowanych aplikacji w synchronizacji.\n        - Obsługuje akcje otwierania, odinstalowywania i instalowania starszych wersji zarządzanych aplikacji.\n\n---\n\n## ✅ Zalety / Dlaczego warto używać GitHub Store?\n\n- **Koniec z przeszukiwaniem wydań na GitHubie**\n  Zobacz tylko repozytoria, które faktycznie dystrybuują pliki binarne dla Twojej platformy.\n\n- **Wie, co zainstalowałeś**\n  Śledzi aplikacje zainstalowane przez GitHub Store (Android) i podkreśla, gdy dostępne są nowe wydania, abyś mógł je zaktualizować bez ponownego przeszukiwania GitHuba.\n\n- **Zawsze aktualne**\n  Instalacje domyślnie używają najnowszego opublikowanego wydania, z opcją przeglądania i instalowania z\n  dowolnego wcześniejszego wydania za pomocą selektora wydań.\n\n- **Spójne doświadczenie na wszystkich platformach**\n  Ten sam interfejs i logika dla Androida i komputera, z natywnym dla platformy zachowaniem instalacyjnym.\n\n- **Open source i rozszerzalny**\n  Napisany w KMP z jasnym rozdziałem między siecią, logiką domenową i interfejsem użytkownika — łatwy do sforkowania,\n  rozszerzenia lub dostosowania.\n\n---\n\n## 🔐 Certyfikat podpisu APK GitHub Store\n\nWszystkie oficjalne wydania GitHub Store są podpisane następującym odciskiem certyfikatu:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 Konfiguracja GitHub OAuth\n\n**Podsumowanie**\n1. Utwórz aplikację GitHub OAuth\n2. Skopiuj **Client ID**\n3. Wklej go do `local.properties`\n\n<details>\n<summary><strong>Pokaż pełną instrukcję konfiguracji</strong></summary>\n\n  <br/>\n\n### 1 - Utwórz aplikację GitHub OAuth\nPrzejdź do:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Pole                           | Wartość                                     |\n| ------------------------------ | ------------------------------------------- |\n| **Application name**           | Dowolna nazwa (np. *GitHub Store Dev*)      |\n| **Homepage URL** | `https://github.com/username/repo_name`                   |\n| **Authorization callback URL** | `githubstore://callback`                    |\n\nNastępnie kliknij **Create application**.\n\n### 2 - Skopiuj swój Client ID\nPo utworzeniu aplikacji GitHub wyświetli:\n- **Client ID** ← tego potrzebujesz\n- **Client Secret** ← ❗ NIE jest wymagany w tym projekcie\n\n### 3 - Dodaj go do projektu\nOtwórz plik `local.properties` w swoim projekcie (katalog główny projektu) i dodaj:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Zsynchronizuj i uruchom\nZsynchronizuj projekt i uruchom aplikację. Teraz powinieneś móc zalogować się przez GitHub.\n\n### ❗ Ważne uwagi\n- `local.properties` **nie jest commitowany do Gita**, więc Twój Client ID pozostaje lokalny.\n- Ten projekt wymaga jedynie **Client ID** (nie Client Secret).\n- Każdy deweloper powinien utworzyć własną aplikację OAuth na potrzeby rozwoju.\n\n</details>\n\n---\n\n## ☕ Wesprzyj projekt\n\n**GitHub Store** osiągnął **ponad 48 000 aktywnych użytkowników** i **ponad 5 500 gwiazdek na GitHubie** — i jest **w 100% darmowy**, bez reklam, bez śledzenia i bez funkcji premium.\n\nBuduję go i utrzymuję całkowicie samodzielnie, kończąc jednocześnie szkołę średnią. Twoje wsparcie (nawet 3$) pomaga mi:\n\n✅ **Utrzymywać aplikację wolną od błędów** — odpowiadać na zgłoszenia i szybko wysyłać poprawki\n✅ **Dodawać funkcje zgłaszane przez społeczność** — implementować to, czego użytkownicy naprawdę potrzebują\n✅ **Utrzymywać infrastrukturę** — serwery, API i koszty wdrożenia\n\n### 💖 Sposoby wsparcia\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**Nie możesz teraz wesprzeć finansowo?** Nic nie szkodzi! Możesz też pomóc:\n- ⭐ **Dając gwiazdkę temu repozytorium** — pomaga innym odkryć GitHub Store\n- 🐛 **Zgłaszając błędy** — ulepszasz aplikację dla wszystkich\n- 📢 **Udostępniając znajomym** — rozpowszechniaj informacje wśród innych deweloperów\n- 💬 **Dołączając do naszego [Discorda](https://discord.gg/x9Cvh2Z9qS)** — Twoje opinie kształtują plan rozwoju\n\nKażda forma wsparcia — finansowa lub nie — wiele znaczy i utrzymuje ten projekt przy życiu. Dziękuję!\n\n---\n\n## ⚠️ Zastrzeżenie\n\nGitHub Store jedynie pomaga odkrywać i pobierać pliki wydań, które są już opublikowane na\nGitHubie przez zewnętrznych deweloperów.\nZawartość, bezpieczeństwo i zachowanie tych pobrań leżą wyłącznie w gestii ich\nodpowiednich autorów i dystrybutorów, a nie tego projektu.\n\nKorzystając z GitHub Store, rozumiesz i akceptujesz, że instalujesz i uruchamiasz jakiekolwiek pobrane oprogramowanie\nna własne ryzyko.\nTen projekt nie sprawdza, nie weryfikuje ani nie gwarantuje, że jakikolwiek instalator jest bezpieczny, wolny od złośliwego oprogramowania lub\nodpowiedni do jakiegokolwiek konkretnego celu.\n\n---\n\n## Historia gwiazdek\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 Licencja\n\nGitHub Store jest dystrybuowany na warunkach **Licencji Apache, wersja 2.0**.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n"
  },
  {
    "path": "docs/README-RU.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Обзор проекта\n\nGitHub Store — это кросс-платформенный магазин приложений для релизов GitHub, созданный для упрощения поиска и установки программного обеспечения с открытым исходным кодом. Он автоматически обнаруживает устанавливаемые бинарные файлы (APK, EXE, DMG, AppImage, DEB, RPM), предлагает установку в один клик, отслеживает обновления и представляет информацию о репозиториях в чистом интерфейсе в стиле магазина приложений.\n\nСоздан на базе Kotlin Multiplatform и Compose Multiplatform для платформ Android и Desktop.\n\n</div>\n\n> [!CAUTION]\n> Свободный и открытый Android под угрозой. Google превратит Android в закрытую платформу, ограничивая вашу основную свободу устанавливать приложения по своему выбору. Заявите о своей позиции – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Вики и ресурсы\n\nОбратитесь к [Вики](https://github.com/OpenHub-Store/GitHub-Store/wiki) GitHub Store для часто задаваемых вопросов и полезной информации\n\n🌐 **Веб-сайт:** [github-store.org](https://github-store.org)\n💬 **Discord:** [Присоединяйтесь к сообществу](https://discord.gg/x9Cvh2Z9qS)\n📜 **Политика конфиденциальности:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Правовая информация\n\nGitHub Store — это независимый проект с открытым исходным кодом, не связанный с GitHub, Inc.  \nНазвание описывает функциональность приложения (обнаружение релизов GitHub) и не подразумевает владения товарным знаком.  \nGitHub® является зарегистрированным товарным знаком GitHub, Inc.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 Скачать\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **Пользователи macOS:** Вы можете увидеть предупреждение о том, что Apple не может проверить GitHub Store. Это происходит потому, что приложение распространяется за пределами App Store и ещё не нотаризовано. Разрешите его через Системные настройки → Конфиденциальность и безопасность → Всё равно открыть.\n\n---\n\n<p align=\"center\">\n\n# 🏆 Упоминания в СМИ\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">Top 20 лучших приложений для Android 2026</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Top 12 магазинов приложений лучше Google Play Store</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Избранный проект</a>\n</p>\n\n---\n\n## 🚀 Возможности\n\n- **Умное обнаружение**\n    - Разделы на главном экране для проектов \"Trending\", \"Hot Release\" и \"Most Popular\" с фильтрами по времени.\n    - Отображаются только репозитории с действительными устанавливаемыми файлами.\n    - Оценка тем с учётом платформы, чтобы пользователи Android/десктопа видели релевантные приложения первыми.\n    - Обновлённый поиск с улучшенным ранжированием по релевантности и производительности.\n\n- **Браузер релизов и установка**\n    - Селектор релизов для просмотра и установки из любого релиза, а не только последнего.\n    - Получает все релизы каждого репозитория.\n    - Действие «Установить последнюю версию» в один клик, а также выпадающий список всех доступных релизов и их установщиков.\n    - Возможность ручной установки с автоматическими проверками совместимости.\n\n- **Подробный экран деталей**\n    - Название приложения, версия и кнопка «Поделиться».\n    - Звёзды, форки, открытые issues.\n    - Отрендеренное содержимое README («О приложении»).\n    - Примечания к релизу в формате Markdown для любого выбранного релиза.\n    - Список установщиков с метками платформ и размерами файлов.\n    - Поддержка глубоких ссылок — открывайте детали репозитория напрямую через URL.\n    - Экран профиля разработчика для просмотра репозиториев и активности разработчика.\n\n- **Управление приложениями**\n    - Открывайте, удаляйте и откатывайте версии установленных приложений прямо из GitHub Store.\n    - Android: совпадение архитектуры APK (armv7/armv8), мониторинг пакетов и отслеживание обновлений.\n    - Десктоп (Windows/macOS/Linux): загрузка установщиков в папку «Загрузки» пользователя и открытие с помощью обработчика по умолчанию.\n\n- **Избранные репозитории**\n    - Сохраняйте и просматривайте ваши избранные репозитории GitHub из приложения.\n\n- **Сеть и производительность**\n    - Поддержка динамического прокси для настраиваемой маршрутизации сети.\n    - Улучшенная система кэширования для более быстрой загрузки и снижения использования API.\n\n---\n\n## 🔍 Как моё приложение появится в GitHub Store?\n\nGitHub Store не использует никакой частной индексации или ручных правил курирования.  \nВаш проект может появиться автоматически, если он соответствует следующим условиям:\n\n1. **Публичный репозиторий на GitHub**\n    - Видимость должна быть `public`.\n\n2. **Устанавливаемые файлы в последнем релизе**\n    - Последний релиз должен содержать как минимум один файл с совместимым расширением:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store игнорирует автоматически сгенерированные архивы исходного кода (`Source code (zip)` /\n      `Source code (tar.gz)`).\n\n3. **Обнаружимость через поиск / topics**\n    - Репозитории получаются через публичный API поиска GitHub.\n    - Topics, язык и описание помогают в ранжировании:\n        - Приложения для Android: topics вроде `android`, `mobile`, `apk`.\n        - Десктопные приложения: topics вроде `desktop`, `windows`, `linux`, `macos`, `compose-desktop`,\n          `electron`.\n    - Наличие хотя бы нескольких звёзд увеличивает вероятность появления в разделах Trending/Hot Release/Most Popular.\n\nЕсли ваш репозиторий соответствует этим условиям, GitHub Store может найти его через поиск и отобразить\nавтоматически, без необходимости ручной подачи заявки.\n\n---\n\n## ✅ Преимущества / Зачем использовать GitHub Store?\n\n- **Больше не нужно копаться в релизах GitHub**\n  Вы видите только репозитории, которые действительно распространяют бинарные файлы для вашей платформы.\n\n- **Знает, что вы установили**\n  Отслеживает приложения, установленные через GitHub Store (Android), и уведомляет о наличии новых релизов, чтобы вы могли обновиться без повторного поиска на GitHub.\n\n- **Всегда актуально**\n  Установки по умолчанию используют последний опубликованный релиз, с возможностью просмотра и установки из\n  любого предыдущего релиза через селектор релизов.\n\n- **Открытый исходный код и расширяемость**  \n  Написан на KMP с чётким разделением сети, доменной логики и пользовательского интерфейса — легко форкнуть,\n  расширить или адаптировать.\n\n---\n\n## 🔐 Сертификат подписи APK GitHub Store\n\nВсе официальные релизы GitHub Store подписаны следующим отпечатком сертификата:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 Настройка GitHub OAuth\n\n**Краткое описание**\n1. Создайте GitHub OAuth App\n2. Скопируйте **Client ID**\n3. Добавьте его в `local.properties`\n\n<details>\n<summary><strong>Показать полное руководство по настройке</strong></summary>\n\n  <br/>\n\n### 1 - Создать GitHub OAuth App\nПерейдите в:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Поле                           | Значение                                    |\n| ------------------------------ | ------------------------------------------- |\n| **Application name**           | Любое на ваш выбор (напр. *GitHub Store Dev*) |\n| **Homepage URL** | `https://github.com/username/repo_name`                   |\n| **Authorization callback URL** | `githubstore://callback`                    |\n\nЗатем нажмите **Create application**.\n\n### 2 - Скопировать Client ID\nПосле создания приложения GitHub покажет:\n- **Client ID** ← это то, что вам нужно\n- **Client Secret** ← ❗ НЕ требуется для этого проекта\n\n### 3 - Добавить в проект\nОткройте файл `local.properties` вашего проекта (корень проекта) и добавьте:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Синхронизировать и запустить\nСинхронизируйте проект и запустите приложение. Теперь вы сможете войти через GitHub.\n\n### ❗ Важные замечания\n- `local.properties` **не загружается в Git**, поэтому ваш Client ID остаётся локальным.\n- Этот проект требует только **Client ID** (не Client Secret).\n- Каждый разработчик должен создать собственное OAuth-приложение для разработки.\n\n</details>\n\n---\n\n## ☕ Поддержать проект\n\nGitHub Store создан и поддерживается старшеклассником. Ваша поддержка помогает ему:\n\n✅ **Поддерживать приложение без ошибок** — отвечать на issues и быстро выпускать исправления  \n✅ **Добавлять функции по запросам сообщества** — реализовывать то, что действительно нужно пользователям\n\n### 💖 Способы поддержки\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**Не можете поддержать финансово прямо сейчас?** Ничего страшного! Вы также можете помочь:\n- ⭐ **Поставив звезду этому репозиторию** — помогает другим открыть для себя GitHub Store\n- 🐛 **Сообщая об ошибках** — улучшает приложение для всех\n- 📢 **Поделившись с друзьями** — расскажите другим разработчикам и знакомым!\n- 💬 **Присоединившись к нашему [Discord](https://discord.gg/x9Cvh2Z9qS)** — ваши отзывы формируют план развития\n\nЛюбая форма поддержки — финансовая или нет — значит многое и помогает проекту жить. Спасибо!\n\n---\n\n## ⚠️ Отказ от ответственности\n\nGitHub Store лишь помогает вам находить и скачивать файлы релизов, которые уже опубликованы на\nGitHub сторонними разработчиками.  \nСодержание, безопасность и поведение этих загрузок являются исключительной ответственностью их\nавторов и распространителей, а не данного проекта.\n\nИспользуя GitHub Store, вы понимаете и соглашаетесь с тем, что устанавливаете и запускаете любое загруженное\nпрограммное обеспечение на свой страх и риск.  \nДанный проект не проверяет, не подтверждает и не гарантирует, что какой-либо установщик является безопасным,\nсвободным от вредоносного ПО или подходящим для какой-либо конкретной цели.\n\n---\n\n## История звёзд\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 Лицензия\n\nGitHub Store распространяется под **Лицензией Apache, Версия 2.0**.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-TR.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ Projeye Genel Bakış\n\nGitHub Store, açık kaynaklı yazılımları keşfetmeyi ve yüklemeyi kolaylaştırmak için tasarlanmış, GitHub sürümleri için çok platformlu bir uygulama mağazasıdır. Kurulabilir ikili dosyaları (APK, EXE, DMG, AppImage, DEB, RPM) otomatik olarak algılar, tek tıkla kurulum sunar, güncellemeleri takip eder ve depo bilgilerini temiz bir uygulama mağazası tarzı arayüzde sunar.\n\nAndroid ve Desktop platformları için Kotlin Multiplatform ve Compose Multiplatform ile geliştirilmiştir.\n\n</div>\n\n> [!CAUTION]\n> Özgür ve Açık Kaynaklı Android tehdit altında. Google, Android'i kapalı bir platforma dönüştürerek istediğiniz uygulamaları yükleme özgürlüğünüzü kısıtlayacak. Sesinizi duyurun – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki ve Kaynaklar\n\nSSS ve yararlı bilgiler için GitHub Store [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) sayfasına göz atın\n\n🌐 **Web sitesi:** [github-store.org](https://github-store.org)\n💬 **Discord:** [Topluluğa katılın](https://discord.gg/x9Cvh2Z9qS)\n📜 **Gizlilik Politikası:** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 Yasal Uyarı\n\nGitHub Store, GitHub, Inc. ile bağlantısı olmayan bağımsız bir açık kaynak projesidir.  \nİsim, uygulamanın işlevselliğini (GitHub sürümlerini keşfetme) tanımlamakta olup herhangi bir marka sahipliği iddiası taşımamaktadır.  \nGitHub®, GitHub, Inc.'in tescilli markasıdır.\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 İndir\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS Kullanıcıları:** Apple'ın GitHub Store'u doğrulayamadığını belirten bir uyarı görebilirsiniz. Bu, uygulamanın App Store dışında dağıtılması ve henüz notarize edilmemiş olması nedeniyle gerçekleşir. Sistem Ayarları → Gizlilik ve Güvenlik → Yine de Aç yolunu izleyerek izin verin.\n\n---\n\n<p align=\"center\">\n\n# 🏆 Öne Çıkarıldığı Yerler\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen:</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">2026'nın En İyi 20 Android Uygulaması</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">Google Play Store'dan Daha İyi 12 Uygulama Mağazası</a>\n</br>\n<strong>HelloGitHub:</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">Öne Çıkan Proje</a>\n</p>\n\n---\n\n## 🚀 Özellikler\n\n- **Akıllı keşif**\n    - Zaman tabanlı filtrelerle \"Trending\", \"Hot Release\" ve \"Most Popular\" projeleri için ana sayfa bölümleri.\n    - Yalnızca geçerli kurulabilir dosyalara sahip depolar gösterilir.\n    - Android/masaüstü kullanıcılarının önce ilgili uygulamaları görmesi için platforma duyarlı konu puanlaması.\n    - Geliştirilmiş alaka sıralaması ve performans ile yenilenmiş arama.\n\n- **Sürüm tarayıcı ve kurulumlar**\n    - Yalnızca en son sürümden değil, herhangi bir sürümden göz atıp kurulum yapabileceğiniz sürüm seçici.\n    - Her depo için tüm sürümleri getirir.\n    - Tek \"En son sürümü kur\" eylemi ve tüm mevcut sürümlerin ve kurucularının genişletilebilir listesi.\n    - Otomatik uyumluluk kontrolleriyle manuel kurulum seçeneği.\n\n- **Zengin ayrıntılar ekranı**\n    - Uygulama adı, sürüm ve paylaşma eylemi.\n    - Yıldızlar, fork'lar, açık sorunlar.\n    - İşlenmiş README içeriği (\"Bu uygulama hakkında\").\n    - Seçilen herhangi bir sürüm için Markdown biçimlendirmeli sürüm notları.\n    - Platform etiketleri ve dosya boyutlarıyla kurucuların listesi.\n    - Derin bağlantı desteği — depo ayrıntılarını doğrudan URL üzerinden açın.\n    - Bir geliştiricinin depolarını ve etkinliğini keşfetmek için geliştirici profil ekranı.\n\n- **Uygulama yönetimi**\n    - Kurulu uygulamaları doğrudan GitHub Store üzerinden açın, kaldırın ve sürüm düşürün.\n    - Android: APK mimari eşleştirme (armv7/armv8), paket izleme ve güncelleme takibi.\n    - Masaüstü (Windows/macOS/Linux): kurucuları kullanıcının İndirilenler klasörüne indirir ve varsayılan işleyici ile açar.\n\n- **Yıldızlı depolar**\n    - Uygulama içinden yıldızlı GitHub depolarınızı kaydedin ve göz atın.\n\n- **Ağ ve performans**\n    - Yapılandırılabilir ağ yönlendirmesi için dinamik proxy desteği.\n    - Daha hızlı yükleme ve daha az API kullanımı için geliştirilmiş önbellekleme sistemi.\n\n---\n\n## 🔍 Uygulamam GitHub Store'da nasıl görünür?\n\nGitHub Store herhangi bir özel indeksleme ya da manuel kürasyon kuralı kullanmaz.  \nProjeniz şu koşulları sağlıyorsa otomatik olarak görünebilir:\n\n1. **GitHub'da herkese açık depo**\n    - Görünürlük `public` olarak ayarlanmış olmalıdır.\n\n2. **En son sürümde kurulabilir dosyalar**\n    - En son sürüm, desteklenen uzantıya sahip en az bir dosya içermelidir:\n        - Android: `.apk`\n        - Windows: `.exe`, `.msi`\n        - macOS: `.dmg`, `.pkg`\n        - Linux: `.deb`, `.rpm`, `.AppImage`\n    - GitHub Store, GitHub'ın otomatik oluşturduğu kaynak arşivlerini (`Source code (zip)` / `Source code (tar.gz)`) yoksayar.\n\n3. **Arama / konular aracılığıyla keşfedilebilir**\n    - Depolar, GitHub'ın genel Arama API'si aracılığıyla getirilir.\n    - Konular, dil ve açıklama sıralamaya yardımcı olur:\n        - Android uygulamaları: `android`, `mobile`, `apk` gibi konular.\n        - Masaüstü uygulamaları: `desktop`, `windows`, `linux`, `macos`, `compose-desktop`, `electron` gibi konular.\n    - En az birkaç yıldıza sahip olmak, Trending/Hot Release/Most Popular bölümlerinde görünme olasılığını artırır.\n\nDeponuz bu koşulları sağlıyorsa GitHub Store onu arama yoluyla bulabilir ve otomatik olarak gösterebilir — manuel gönderim gerekmez.\n\n---\n\n## ✅ Artılar / GitHub Store neden kullanılır?\n\n- **GitHub sürümlerini artık elle aramak yok**\n  Yalnızca platformunuz için gerçekten ikili dosya sunan depoları görün.\n\n- **Ne kurduğunuzu bilir**\n  GitHub Store (Android) üzerinden kurulan uygulamaları takip eder ve yeni sürümler mevcut olduğunda vurgular; böylece GitHub'da tekrar arama yapmadan güncelleyebilirsiniz.\n\n- **Her zaman güncel**\n  Kurulumlar varsayılan olarak en son yayımlanan sürümü kullanır; sürüm seçici aracılığıyla herhangi bir önceki sürümü de göz atıp kurma seçeneği mevcuttur.\n\n- **Açık kaynak ve genişletilebilir**  \n  Ağ, alan mantığı ve kullanıcı arayüzü arasında net bir ayrımla KMP'de yazılmıştır — fork'lamak, genişletmek veya uyarlamak kolaydır.\n\n---\n\n## 🔐 GitHub Store APK İmzalama Sertifikası\n\nTüm resmi GitHub Store sürümleri şu sertifika parmak iziyle imzalanmıştır:\n\nSHA-256:\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth Yapılandırması\n\n**Kısaca**\n1. Bir GitHub OAuth App oluşturun\n2. **Client ID**'yi kopyalayın\n3. `local.properties` dosyasına yapıştırın\n\n<details>\n<summary><strong>Tam kurulum kılavuzunu göster</strong></summary>\n\n  <br/>\n\n### 1 - GitHub OAuth App Oluşturma\nŞuraya gidin:\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| Alan                           | Değer                                           |\n| ------------------------------ | ----------------------------------------------- |\n| **Application name**           | İstediğiniz bir şey (örn. *GitHub Store Dev*)   |\n| **Homepage URL**               | `https://github.com/username/repo_name`         |\n| **Authorization callback URL** | `githubstore://callback`                        |\n\nArdından **Create application** düğmesine tıklayın.\n\n### 2 - Client ID'nizi Kopyalayın\nUygulamayı oluşturduktan sonra GitHub şunları gösterecek:\n- **Client ID** ← ihtiyacınız olan bu\n- **Client Secret** ← ❗ Bu proje için GEREKLİ DEĞİL\n\n### 3 - Projenize Ekleyin\nProjenizin `local.properties` dosyasını (projenin kök dizini) açın ve şunu ekleyin:\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - Senkronize Edin ve Çalıştırın\nProjeyi senkronize edin ve uygulamayı çalıştırın. Artık GitHub ile giriş yapabilmeniz gerekir.\n\n### ❗ Önemli Notlar\n- `local.properties` **Git'e eklenmez**, dolayısıyla Client ID'niz yerel kalır.\n- Bu proje yalnızca **Client ID**'ye ihtiyaç duyar (Client Secret'a değil).\n- Her geliştirici, geliştirme için kendi OAuth App'ini oluşturmalıdır.\n\n</details>\n\n---\n\n## ☕ Projeyi Destekleyin\n\nGitHub Store, lise öğrencisi tarafından geliştirilmekte ve bakımı yapılmaktadır. Desteğiniz ona şunlarda yardımcı olur:\n\n✅ **Uygulamayı hatasız tutmak** — sorunlara yanıt vermek ve düzeltmeleri hızlıca yayımlamak  \n✅ **Topluluk tarafından istenen özellikleri eklemek** — kullanıcıların gerçekten ihtiyaç duyduklarını uygulamak\n\n### 💖 Destek Yolları\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**Şu an sponsor olamazsanız?** Sorun değil! Yine de şu şekillerde yardımcı olabilirsiniz:\n- ⭐ **Bu depoya yıldız vermek** — başkalarının GitHub Store'u keşfetmesine yardımcı olur\n- 🐛 **Hata bildirmek** — uygulamayı herkes için daha iyi hale getirir\n- 📢 **Arkadaşlarınızla paylaşmak** — diğer geliştiricilere ve yakınlarınıza söyleyin!\n- 💬 **[Discord](https://discord.gg/x9Cvh2Z9qS)'umuza katılmak** — geri bildirimleriniz yol haritasını şekillendirir\n\nHer türlü destek — finansal ya da değil — çok büyük anlam taşır ve bu projeyi yaşatır. Teşekkürler!\n\n---\n\n## ⚠️ Sorumluluk Reddi\n\nGitHub Store, yalnızca üçüncü taraf geliştiriciler tarafından GitHub'da zaten yayımlanmış olan sürüm dosyalarını keşfetmenize ve indirmenize yardımcı olur.  \nBu indirmelerin içeriği, güvenliği ve davranışı, bu projenin değil, ilgili yazarların ve dağıtımcıların münhasır sorumluluğundadır.\n\nGitHub Store'u kullanarak, indirilen herhangi bir yazılımı kendi riskinizle kurduğunuzu ve çalıştırdığınızı anlıyor ve kabul ediyorsunuz.  \nBu proje, herhangi bir kurucunun güvenli, kötü amaçlı yazılımdan arındırılmış ya da belirli bir amaç için uygun olduğunu incelemez, doğrulamaz veya garanti etmez.\n\n---\n\n## Yıldız Geçmişi\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 Lisans\n\nGitHub Store **Apache Lisansı, Sürüm 2.0** kapsamında yayımlanacaktır.\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "docs/README-ZH.md",
    "content": "<div align=\"center\">\n</br>\n<img src=\"../media-resources/app_icon.png\" width=\"200\" />\n\n</div>\n\n<div align=\"center\">\n\n# GitHub Store\n\n</div>\n\n</br>\n\n<p align=\"center\">\n  <img alt=\"API\" src=\"https://img.shields.io/badge/Api%2024+-50f270?logo=android&logoColor=black&style=for-the-badge\"/>\n  <img alt=\"Kotlin\" src=\"https://img.shields.io/badge/Kotlin-Multiplatform-a503fc?logo=kotlin&logoColor=white&style=for-the-badge\"/>\n  <img alt=\"Compose Multiplatform\" src=\"https://img.shields.io/static/v1?style=for-the-badge&message=Compose+Multiplatform&color=4285F4&logo=Jetpack+Compose&logoColor=FFFFFF&label=\"/> \n  <img alt=\"Material\" src=\"https://custom-icon-badges.demolab.com/badge/material%20you-lightblue?style=for-the-badge&logoColor=333&logo=material-you\"/>\n</p>\n\n</br>\n</br>\n\n<img src=\"https://img.shields.io/github/downloads/OpenHub-Store/GitHub-Store/total?color=aeff4d&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmRvd25sb2FkPC90aXRsZT48cGF0aCBkPSJNNSwyMEgxOVYxOEg1TTE5LDlIMTVWM0g5VjlINUwxMiwxNkwxOSw5WiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&label=Downloads&labelColor=4b731a\"/>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/stargazers\">\n<img src=\"https://img.shields.io/github/stars/OpenHub-Store/GitHub-Store?color=ffff00&style=for-the-badge&labelColor=a1a116&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPnN0YXI8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxNy4yN0wxOC4xOCwyMUwxNi41NCwxMy45N0wyMiw5LjI0TDE0LjgxLDguNjJMMTIsMkw5LjE5LDguNjJMMiw5LjI0TDcuNDUsMTMuOTdMNS44MiwyMUwxMiwxNy4yN1oiIGZpbGw9IndoaXRlIiAvPjwvc3ZnPg==\"/>\n</a>\n\n<img src=\"https://img.shields.io/badge/65K+-Users-8ce2ff?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRpdGxlPmFjY291bnQtZ3JvdXA8L3RpdGxlPjxwYXRoIGQ9Ik0xMiwxMi43NUM3LDEyLjc1IDMsMTMuMzUgMywxNi4yNVYxOEgyMVYxNi4yNUMyMSwxMy4zNSAxNywxMi43NSAxMiwxMi43NU0xNyw3QTE3LDE3IDAgMCwxIDE3LDdNMjEsMTYuMjVWMThIMjRWMTYuMjVDMjQsMTQuNDMgMjEuNSwxMy44NyAxOSwxMy41QzIwLjEyLDE0LjEgMjEsMTUgMjEsMTYuMjVNMiw3QTIsMiAwIDAsMSA0LDVIMjBBMiwyIDAgMCwxIDIyLDdBMiwyIDAgMCwxIDIwLDlINEEyLDIgMCAwLDEgMiw3TTEyLDEwQTMsMyAwIDAsMSA5LDdBMywzIDAgMCwxIDEyLDRBMywzIDAgMCwxIDE1LDdBMywzIDAgMCwxIDEyLDEwWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+&labelColor=0782ab\"/>\n\n</br>\n</br>\n\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases/latest\">\n  <img src=\"https://img.shields.io/github/v/release/OpenHub-Store/GitHub-Store?color=a1168e&include_prereleases&logo=github&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n<a href=\"https://f-droid.org/packages/zed.rainxch.githubstore\">\n  <img src=\"https://img.shields.io/f-droid/v/zed.rainxch.githubstore?color=a1168e&include_prereleases&logo=FDROID&style=for-the-badge&labelColor=700f63\"/>\n</a>\n\n</br>\n</br>\n\n<p align=\"center\"> \n <a href=\"https://trendshift.io/repositories/22313\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/22313\" alt=\"OpenHub-Store%2FGitHub-Store | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n<a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\" target=\"_blank\">\n  <img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=OpenHub-Store%2FGitHub-Store&claim_uid=&theme=dark\" alt=\"Featured｜HelloGitHub\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" />\n</a>\n</p>\n\n</p>\n\n<div align=\"center\">\n\n# 🗺️ 项目概述\n\nGitHub Store 是一款专为 GitHub Releases 设计的跨平台应用商店，旨在简化开源软件的发现与安装过程。它能自动检测可安装的二进制文件（APK、EXE、DMG、AppImage、DEB、RPM），提供一键安装、更新追踪，并以整洁的应用商店风格界面展示仓库信息。\n\n基于 Kotlin Multiplatform 和 Compose Multiplatform 开发，支持 Android 和桌面平台。\n\n</div>\n\n> [!CAUTION]\n> 自由开源的 Android 正面临威胁。Google 将把 Android 变成一个封闭平台，限制你自由安装所选应用的基本权利。让你的声音被听到 – [keepandroidopen.org](https://keepandroidopen.org/).\n\n<p align=\"middle\">\n    <img src=\"../media-resources/banner.jpeg\" width=\"99%\" />\n    <img src=\"../media-resources/screenshots/mobile/01.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/02.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/03.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/04.jpg\" width=\"18%\" />\n    <img src=\"../media-resources/screenshots/mobile/05.jpg\" width=\"18%\" />\n</p>\n\n<div align=\"center\">\n\n# 📔 Wiki 与资源\n\n请查阅 GitHub Store [Wiki](https://github.com/OpenHub-Store/GitHub-Store/wiki) 获取常见问题解答和实用信息\n\n🌐 **官方网站：** [github-store.org](https://github-store.org)\n💬 **Discord：** [加入社区](https://discord.gg/x9Cvh2Z9qS)\n📜 **隐私政策：** [github-store.org/privacy-policy](https://github-store.org/privacy-policy/)\n\n</div>\n\n---\n\n<div align=\"center\">\n\n### 📋 法律声明\n\nGitHub Store 是一个独立的开源项目，与 GitHub, Inc. 无关。  \n该名称用于描述应用的功能（发现 GitHub Releases），不代表对商标的所有权主张。  \nGitHub® 是 GitHub, Inc. 的注册商标。\n\n</div>\n\n---\n\n<p align=\"center\">\n\n# 🔃 下载\n</p>\n\n<p align=\"center\">\n<a href=\"https://github.com/OpenHub-Store/GitHub-Store/releases\">\n   <img src=\"https://i.ibb.co/q0mdc4Z/get-it-on-github.png\" height=\"70\"/>\n</a>\n\n<a href=\"https://f-droid.org/en/packages/zed.rainxch.githubstore/\">\n   <img src=\"https://f-droid.org/badge/get-it-on.png\" height=\"80\"/>\n</a>\n</p>\n\n<p align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/OpenHub-Store/GitHub-Store/\">\n  <img src=\"https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png\" height=\"60\" alt=\"Get it on Obtainium\">\n</a>\n\n<a href=\"https://github-store.org/app?repo=OpenHub-Store/GitHub-Store\">\n  <img src=\"../media-resources/ghs_download_badge.png\" alt=\"Get it on GitHub Store\" height=\"64\">\n</a>\n</p>\n\n> [!IMPORTANT]\n> **macOS 用户：** 你可能会看到 Apple 无法验证 GitHub Store 的警告。这是因为该应用在 App Store 之外分发，尚未经过公证。请通过「系统设置 → 隐私与安全性 → 仍然打开」来允许运行。\n\n---\n\n<p align=\"center\">\n\n# 🏆 媒体报道\n</p>\n<p align=\"center\">\n<a href=\"https://www.youtube.com/@howtomen\">\n  <img src=\"https://img.shields.io/badge/HowToMen-red?style=for-the-badge&logo=youtube&logoColor=white\" alt=\"Featured by HowToMen\">\n</a>\n</br>\n<strong>HowToMen：</strong> <a href=\"https://www.youtube.com/watch?v=7favc9MDedQ\">2026 年最佳 Android 应用 TOP 20</a> | <a href=\"https://www.youtube.com/watch?v=VR-MEwPDw4k\">比 Google Play 商店更好的 12 个应用商店</a>\n</br>\n<strong>HelloGitHub：</strong> <a href=\"https://hellogithub.com/en/repository/OpenHub-Store/GitHub-Store\">精选项目</a>\n</p>\n\n---\n\n## 🚀 功能特性\n\n- **智能发现**\n    - 首页分为「Trending（趋势）」「Hot Release（热门发布）」「Most Popular（最受欢迎）」三大版块，支持按时间筛选。\n    - 仅显示拥有有效可安装文件的仓库。\n    - 平台感知话题评分，让 Android/桌面用户优先看到相关应用。\n    - 全面升级的搜索功能，相关性排名和性能均有显著提升。\n\n- **Release 浏览器与安装**\n    - Release 选择器，可浏览并安装任意版本，而非仅限最新版。\n    - 获取每个仓库的全部 Release 记录。\n    - 一键「安装最新版」操作，以及所有可用 Release 及其安装包的展开列表。\n    - 手动安装选项，并附带自动兼容性检测。\n\n- **丰富的详情页面**\n    - 应用名称、版本号及分享功能。\n    - Star 数、Fork 数、未关闭的 Issue 数。\n    - 渲染后的 README 内容（「关于此应用」）。\n    - 所选 Release 的 Markdown 格式发行说明。\n    - 带平台标签和文件大小的安装包列表。\n    - 深度链接支持 — 通过 URL 直接打开仓库详情。\n    - 开发者主页，可浏览开发者的仓库和动态。\n\n- **应用管理**\n    - 直接在 GitHub Store 中打开、卸载及降级已安装的应用。\n    - Android：APK 架构匹配（armv7/armv8）、软件包监控及更新追踪。\n    - 桌面端（Windows/macOS/Linux）：将安装包下载到用户的「下载」文件夹，并以默认程序打开。\n\n- **收藏的仓库**\n    - 在应用内保存并浏览你在 GitHub 上收藏的仓库。\n\n- **网络与性能**\n    - 动态代理支持，可配置网络路由。\n    - 增强的缓存系统，加快加载速度，减少 API 用量。\n\n---\n\n## 🔍 我的应用如何出现在 GitHub Store 中？\n\nGitHub Store 不使用任何私有索引或手动策划规则。  \n只要满足以下条件，你的项目便可自动显示：\n\n1. **GitHub 上的公开仓库**\n    - 可见性必须设置为 `public`。\n\n2. **最新 Release 中包含可安装文件**\n    - 最新 Release 中至少包含一个受支持扩展名的文件：\n        - Android：`.apk`\n        - Windows：`.exe`、`.msi`\n        - macOS：`.dmg`、`.pkg`\n        - Linux：`.deb`、`.rpm`、`.AppImage`\n    - GitHub Store 会忽略 GitHub 自动生成的源码压缩包（`Source code (zip)` / `Source code (tar.gz)`）。\n\n3. **可通过搜索 / 话题被发现**\n    - 仓库通过 GitHub 公开搜索 API 获取。\n    - 话题、编程语言和描述影响排名：\n        - Android 应用：`android`、`mobile`、`apk` 等话题。\n        - 桌面应用：`desktop`、`windows`、`linux`、`macos`、`compose-desktop`、`electron` 等话题。\n    - 拥有至少几个 Star 可以提高出现在 Trending/Hot Release/Most Popular 版块的概率。\n\n只要你的仓库满足这些条件，GitHub Store 便可通过搜索自动找到并展示它 — 无需手动提交。\n\n---\n\n## ✅ 优势 / 为什么使用 GitHub Store？\n\n- **无需再手动翻找 GitHub Releases**\n  只看那些真正为你的平台提供二进制文件的仓库。\n\n- **了解你安装了什么**\n  追踪通过 GitHub Store 安装的应用（Android），并在有新版本时高亮提示，让你无需再回 GitHub 搜索即可完成更新。\n\n- **始终保持最新**\n  安装默认使用最新发布的 Release，也可通过 Release 选择器浏览并安装任意历史版本。\n\n- **开源且可扩展**  \n  使用 KMP 编写，网络层、业务逻辑层与 UI 层清晰分离 — 易于 Fork、扩展或定制。\n\n---\n\n## 🔐 GitHub Store APK 签名证书\n\n所有官方 GitHub Store Release 均使用以下证书指纹签名：\n\nSHA-256：\n`B7:F2:8E:19:8E:48:C1:93:B0:38:C6:5D:92:DD:F7:BC:07:7B:0D:B5:9E:BC:9B:25:0A:6D:AC:48:C1:18:03:CA`\n\n---\n\n## 🔑 GitHub OAuth 配置\n\n**简要步骤**\n1. 创建一个 GitHub OAuth App\n2. 复制 **Client ID**\n3. 填入 `local.properties`\n\n<details>\n<summary><strong>查看完整配置指南</strong></summary>\n\n  <br/>\n\n### 1 - 创建 GitHub OAuth App\n前往：\n**GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**\n\n| 字段                           | 值                                              |\n| ------------------------------ | ----------------------------------------------- |\n| **Application name**           | 任意名称（例如 *GitHub Store Dev*）             |\n| **Homepage URL**               | `https://github.com/username/repo_name`         |\n| **Authorization callback URL** | `githubstore://callback`                        |\n\n然后点击 **Create application**。\n\n### 2 - 复制 Client ID\n创建完成后，GitHub 将显示：\n- **Client ID** ← 这是你需要的\n- **Client Secret** ← ❗ 本项目不需要\n\n### 3 - 添加到项目\n打开项目根目录下的 `local.properties` 文件，添加：\n```properties\nGITHUB_CLIENT_ID=YOUR_CLIENT_ID_HERE\n```\n\n### 4 - 同步并运行\n同步项目并运行应用。现在你应该可以使用 GitHub 账号登录了。\n\n### ❗ 重要说明\n- `local.properties` **不会提交到 Git**，因此你的 Client ID 只保留在本地。\n- 本项目只需要 **Client ID**（不需要 Client Secret）。\n- 每位开发者应为自己的开发环境创建独立的 OAuth App。\n\n</details>\n\n---\n\n## ☕ 支持项目\n\nGitHub Store 由一名高中生开发和维护。你的支持能帮助他：\n\n✅ **保持应用无 Bug** — 响应 Issue 并快速发布修复  \n✅ **添加社区请求的功能** — 实现用户真正需要的内容\n\n### 💖 支持方式\n\n<a href=\"https://www.buymeacoffee.com/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-☕%20$3-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me a Coffee\">\n</a>\n\n<a href=\"https://github.com/sponsors/rainxchzed\">\n  <img src=\"https://img.shields.io/badge/GitHub%20Sponsors-💖%20Monthly-pink?style=for-the-badge&logo=github&logoColor=white\" alt=\"GitHub Sponsors\">\n</a>\n\n**暂时无法赞助？** 没关系！你仍可以通过以下方式提供帮助：\n- ⭐ **给这个仓库点个 Star** — 帮助更多人发现 GitHub Store\n- 🐛 **反馈 Bug** — 让应用对所有人都更好用\n- 📢 **分享给朋友** — 向其他开发者和朋友安利！\n- 💬 **加入我们的 [Discord](https://discord.gg/x9Cvh2Z9qS)** — 你的反馈将影响开发路线图\n\n无论是金钱还是其他形式的支持，都意义重大，让这个项目得以延续。谢谢！\n\n---\n\n## ⚠️ 免责声明\n\nGitHub Store 仅帮助你发现和下载由第三方开发者已在 GitHub 上发布的 Release 文件。  \n这些下载内容的安全性、行为及合规性由其各自的作者和分发者负责，与本项目无关。\n\n使用 GitHub Store 即表示你理解并同意：安装和运行任何已下载的软件均需自行承担风险。  \n本项目不对任何安装包的安全性、是否包含恶意软件或是否适用于特定用途作出审查、验证或保证。\n\n---\n\n## Star 历史\n\n<a href=\"https://www.star-history.com/#OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&theme=dark&legend=top-left\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=OpenHub-Store/GitHub-Store&type=timeline&legend=top-left\" />\n </picture>\n</a>\n\n![Alt](https://repobeats.axiom.co/api/embed/20367dca127572e9c47db33850979d78df2c6a8b.svg \"Repobeats analytics image\")\n\n## 📄 许可证\n\nGitHub Store 将在 **Apache License, Version 2.0** 下发布。\n\n```\nCopyright 2025 rainxchzed\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this project except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Github Store is a \"play store\" for GitHub releases that helps you discover and install apps directly from GitHub repositories.\n\nThe app automatically discovers repositories that ship installable binaries (APK, EXE, DMG, etc.) and lets you install the latest release with one click.\n\nFEATURES\n\n- Smart discovery with Popular, Recently Updated, and New sections\n- Platform-aware filtering - only shows apps compatible with your device\n- Always installs from the latest published release\n- Rich app details with README, changelog, and statistics\n- GitHub login support for higher API rate limits\n- Material 3 design with dark mode support\n\nBuilt with Kotlin Multiplatform and Compose Multiplatform."
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "App store for GitHub releases - discover and install apps with one click"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "Github Store"
  },
  {
    "path": "feature/apps/CLAUDE.md",
    "content": "# CLAUDE.md - Apps Feature\n\n## Purpose\n\nManages installed applications. Lists all apps installed through GitHub Store, allows launching them, and checks for available updates. Primarily relevant on **Android** (apps section is hidden on Desktop).\n\n## Module Structure\n\n```\nfeature/apps/\n├── domain/\n│   └── repository/AppsRepository.kt   # Installed apps, launch, update check\n├── data/\n│   ├── di/SharedModule.kt            # Koin: appsModule\n│   └── repository/AppsRepositoryImpl.kt  # Implementation using core InstalledAppsRepository\n└── presentation/\n    ├── AppsViewModel.kt               # State management for installed apps list\n    ├── AppsState.kt                   # apps list, loading, error\n    ├── AppsAction.kt                  # Refresh, OpenApp, CheckUpdates, clicks\n    ├── AppsEvent.kt                   # One-off events\n    ├── AppsRoot.kt                    # Main composable (apps list)\n    └── components/                    # App item cards, update badges\n```\n\n## Key Interfaces\n\n```kotlin\ninterface AppsRepository {\n    suspend fun getApps(): Flow<List<InstalledApp>>\n    suspend fun openApp(installedApp: InstalledApp, onCantLaunchApp: () -> Unit = {})\n    suspend fun getLatestRelease(owner: String, repo: String): GithubRelease?\n}\n```\n\n## Navigation\n\nRoute: `GithubStoreGraph.AppsScreen` (data object, no params)\n\n## Implementation Notes\n\n- Uses `InstalledAppsRepository` and `SyncInstalledAppsUseCase` from core/domain\n- `openApp()` uses `AppLauncher` from core/domain to launch the installed app\n- `getLatestRelease()` checks if a newer version is available\n- Platform-specific: `PackageMonitor` and `Installer` handle Android package management\n- The apps section in the home screen bottom nav is only visible on `Platform.ANDROID`\n"
  },
  {
    "path": "feature/apps/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/apps/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.apps.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n                implementation(libs.kotlinx.datetime)\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/apps/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/di/SharedModule.kt",
    "content": "package zed.rainxch.apps.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.apps.data.repository.AppsRepositoryImpl\nimport zed.rainxch.apps.domain.repository.AppsRepository\n\nval appsModule =\n    module {\n        single<AppsRepository> {\n            AppsRepositoryImpl(\n                appLauncher = get(),\n                appsRepository = get(),\n                logger = get(),\n                httpClient = get(),\n                packageMonitor = get(),\n                tweaksRepository = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt",
    "content": "package zed.rainxch.apps.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.http.HttpHeaders\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.serialization.json.Json\nimport zed.rainxch.apps.domain.model.GithubRepoInfo\nimport zed.rainxch.apps.domain.model.ImportResult\nimport zed.rainxch.apps.domain.repository.AppsRepository\nimport zed.rainxch.core.data.dto.GithubRepoNetworkModel\nimport zed.rainxch.core.data.dto.ReleaseNetwork\nimport zed.rainxch.core.data.mappers.toDomain\nimport zed.rainxch.core.data.network.executeRequest\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.DeviceApp\nimport zed.rainxch.core.domain.model.ExportedApp\nimport zed.rainxch.core.domain.model.ExportedAppList\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.InstallSource\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.PackageMonitor\nimport zed.rainxch.core.domain.utils.AppLauncher\nimport kotlin.time.Clock\n\nclass AppsRepositoryImpl(\n    private val appLauncher: AppLauncher,\n    private val appsRepository: InstalledAppsRepository,\n    private val logger: GitHubStoreLogger,\n    private val httpClient: HttpClient,\n    private val packageMonitor: PackageMonitor,\n    private val tweaksRepository: TweaksRepository,\n) : AppsRepository {\n    private val json = Json { ignoreUnknownKeys = true }\n\n    override suspend fun getApps(): Flow<List<InstalledApp>> = appsRepository.getAllInstalledApps()\n\n    override suspend fun openApp(\n        installedApp: InstalledApp,\n        onCantLaunchApp: () -> Unit,\n    ) {\n        val canLaunch = appLauncher.canLaunchApp(installedApp)\n\n        if (canLaunch) {\n            appLauncher\n                .launchApp(installedApp)\n                .onFailure { error ->\n                    logger.error(\"Failed to launch app: ${error.message}\")\n                    onCantLaunchApp()\n                }\n        } else {\n            onCantLaunchApp()\n        }\n    }\n\n    override suspend fun getLatestRelease(\n        owner: String,\n        repo: String,\n    ): GithubRelease? =\n        try {\n            val includePreReleases = tweaksRepository.getIncludePreReleases().first()\n\n            val releases =\n                httpClient\n                    .executeRequest<List<ReleaseNetwork>> {\n                        get(\"/repos/$owner/$repo/releases\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                            parameter(\"per_page\", 10)\n                        }\n                    }.getOrThrow()\n\n            releases\n                .asSequence()\n                .filter { it.draft != true }\n                .filter { includePreReleases || it.prerelease != true }\n                .maxByOrNull { it.publishedAt ?: it.createdAt ?: \"\" }\n                ?.toDomain()\n        } catch (e: RateLimitException) {\n            throw e\n        } catch (e: Exception) {\n            logger.error(\"Failed to fetch latest release for $owner/$repo: ${e.message}\")\n            null\n        }\n\n    override suspend fun getDeviceApps(): List<DeviceApp> = packageMonitor.getAllInstalledApps()\n\n    override suspend fun getTrackedPackageNames(): Set<String> =\n        appsRepository\n            .getAllInstalledApps()\n            .first()\n            .map { it.packageName }\n            .toSet()\n\n    override suspend fun fetchRepoInfo(\n        owner: String,\n        repo: String,\n    ): GithubRepoInfo? =\n        try {\n            val repoModel =\n                httpClient\n                    .executeRequest<GithubRepoNetworkModel> {\n                        get(\"/repos/$owner/$repo\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                        }\n                    }.getOrThrow()\n\n            val includePreReleases = tweaksRepository.getIncludePreReleases().first()\n            val latestTag =\n                try {\n                    val releases =\n                        httpClient\n                            .executeRequest<List<ReleaseNetwork>> {\n                                get(\"/repos/$owner/$repo/releases\") {\n                                    header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                                    parameter(\"per_page\", 5)\n                                }\n                            }.getOrThrow()\n\n                    releases\n                        .asSequence()\n                        .filter { it.draft != true }\n                        .filter { includePreReleases || it.prerelease != true }\n                        .maxByOrNull { it.publishedAt ?: it.createdAt ?: \"\" }\n                        ?.tagName\n                } catch (_: Exception) {\n                    null\n                }\n\n            GithubRepoInfo(\n                id = repoModel.id,\n                name = repoModel.name,\n                owner = repoModel.owner.login,\n                ownerAvatarUrl = repoModel.owner.avatarUrl,\n                description = repoModel.description,\n                language = repoModel.language,\n                htmlUrl = repoModel.htmlUrl,\n                latestReleaseTag = latestTag,\n            )\n        } catch (e: RateLimitException) {\n            throw e\n        } catch (e: Exception) {\n            logger.error(\"Failed to fetch repo info for $owner/$repo: ${e.message}\")\n            null\n        }\n\n    override suspend fun linkAppToRepo(\n        deviceApp: DeviceApp,\n        repoInfo: GithubRepoInfo,\n    ) {\n        val now = Clock.System.now().toEpochMilliseconds()\n\n        val installedApp =\n            InstalledApp(\n                packageName = deviceApp.packageName,\n                repoId = repoInfo.id,\n                repoName = repoInfo.name,\n                repoOwner = repoInfo.owner,\n                repoOwnerAvatarUrl = repoInfo.ownerAvatarUrl,\n                repoDescription = repoInfo.description,\n                primaryLanguage = repoInfo.language,\n                repoUrl = repoInfo.htmlUrl,\n                installedVersion = deviceApp.versionName ?: \"unknown\",\n                installedAssetName = null,\n                installedAssetUrl = null,\n                latestVersion = repoInfo.latestReleaseTag,\n                latestAssetName = null,\n                latestAssetUrl = null,\n                latestAssetSize = null,\n                appName = deviceApp.appName,\n                installSource = InstallSource.MANUAL,\n                installedAt = now,\n                lastCheckedAt = 0L,\n                lastUpdatedAt = now,\n                isUpdateAvailable = false,\n                updateCheckEnabled = true,\n                releaseNotes = null,\n                systemArchitecture = \"\",\n                fileExtension = \"apk\",\n                isPendingInstall = false,\n                installedVersionName = deviceApp.versionName,\n                installedVersionCode = deviceApp.versionCode,\n                signingFingerprint = deviceApp.signingFingerprint,\n            )\n\n        appsRepository.saveInstalledApp(installedApp)\n    }\n\n    override suspend fun exportApps(): String {\n        val apps = appsRepository.getAllInstalledApps().first()\n        val exported =\n            ExportedAppList(\n                version = 1,\n                exportedAt = Clock.System.now().toEpochMilliseconds(),\n                apps =\n                    apps.map { app ->\n                        ExportedApp(\n                            packageName = app.packageName,\n                            repoOwner = app.repoOwner,\n                            repoName = app.repoName,\n                            repoUrl = app.repoUrl,\n                        )\n                    },\n            )\n        return json.encodeToString(ExportedAppList.serializer(), exported)\n    }\n\n    override suspend fun importApps(json: String): ImportResult {\n        val exportedList =\n            try {\n                this@AppsRepositoryImpl.json.decodeFromString(ExportedAppList.serializer(), json)\n            } catch (e: Exception) {\n                logger.error(\"Failed to parse import JSON: ${e.message}\")\n                return ImportResult(imported = 0, skipped = 0, failed = 1)\n            }\n\n        val trackedPackages = getTrackedPackageNames()\n        var imported = 0\n        var skipped = 0\n        var failed = 0\n\n        for (exportedApp in exportedList.apps) {\n            if (exportedApp.packageName in trackedPackages) {\n                skipped++\n                continue\n            }\n\n            try {\n                val repoInfo = fetchRepoInfo(exportedApp.repoOwner, exportedApp.repoName)\n                if (repoInfo == null) {\n                    failed++\n                    continue\n                }\n\n                val systemInfo = packageMonitor.getInstalledPackageInfo(exportedApp.packageName)\n\n                val deviceApp =\n                    DeviceApp(\n                        packageName = exportedApp.packageName,\n                        appName = exportedApp.repoName,\n                        versionName = systemInfo?.versionName,\n                        versionCode = systemInfo?.versionCode ?: 0L,\n                        signingFingerprint = systemInfo?.signingFingerprint,\n                    )\n\n                linkAppToRepo(deviceApp, repoInfo)\n                imported++\n            } catch (e: Exception) {\n                logger.error(\"Failed to import ${exportedApp.repoOwner}/${exportedApp.repoName}: ${e.message}\")\n                failed++\n            }\n        }\n\n        return ImportResult(imported = imported, skipped = skipped, failed = failed)\n    }\n}\n"
  },
  {
    "path": "feature/apps/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/apps/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/apps/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/model/GithubRepoInfo.kt",
    "content": "package zed.rainxch.apps.domain.model\n\ndata class GithubRepoInfo(\n    val id: Long,\n    val name: String,\n    val owner: String,\n    val ownerAvatarUrl: String,\n    val description: String?,\n    val language: String?,\n    val htmlUrl: String,\n    val latestReleaseTag: String?,\n)\n"
  },
  {
    "path": "feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/model/ImportResult.kt",
    "content": "package zed.rainxch.apps.domain.model\n\ndata class ImportResult(\n    val imported: Int,\n    val skipped: Int,\n    val failed: Int,\n)\n"
  },
  {
    "path": "feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt",
    "content": "package zed.rainxch.apps.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.apps.domain.model.GithubRepoInfo\nimport zed.rainxch.apps.domain.model.ImportResult\nimport zed.rainxch.core.domain.model.DeviceApp\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.InstalledApp\n\ninterface AppsRepository {\n    suspend fun getApps(): Flow<List<InstalledApp>>\n\n    suspend fun openApp(\n        installedApp: InstalledApp,\n        onCantLaunchApp: () -> Unit = { },\n    )\n\n    suspend fun getLatestRelease(\n        owner: String,\n        repo: String,\n    ): GithubRelease?\n\n    suspend fun getDeviceApps(): List<DeviceApp>\n\n    suspend fun getTrackedPackageNames(): Set<String>\n\n    suspend fun fetchRepoInfo(owner: String, repo: String): GithubRepoInfo?\n\n    suspend fun linkAppToRepo(deviceApp: DeviceApp, repoInfo: GithubRepoInfo)\n\n    suspend fun exportApps(): String\n\n    suspend fun importApps(json: String): ImportResult\n}\n"
  },
  {
    "path": "feature/apps/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/apps/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.apps.domain)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n\n                implementation(libs.bundles.landscapist)\n                implementation(libs.liquid)\n\n                implementation(libs.kotlinx.collections.immutable)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt",
    "content": "package zed.rainxch.apps.presentation\n\nimport zed.rainxch.apps.presentation.model.InstalledAppUi\nimport zed.rainxch.apps.presentation.model.DeviceAppUi\nimport zed.rainxch.apps.presentation.model.GithubAssetUi\n\n\nsealed interface AppsAction {\n    data object OnNavigateBackClick : AppsAction\n\n    data class OnSearchChange(\n        val query: String,\n    ) : AppsAction\n\n    data class OnOpenApp(\n        val app: InstalledAppUi,\n    ) : AppsAction\n\n    data class OnUpdateApp(\n        val app: InstalledAppUi,\n    ) : AppsAction\n\n    data class OnCancelUpdate(\n        val packageName: String,\n    ) : AppsAction\n\n    data object OnUpdateAll : AppsAction\n\n    data object OnCancelUpdateAll : AppsAction\n\n    data object OnCheckAllForUpdates : AppsAction\n\n    data object OnRefresh : AppsAction\n\n    data class OnNavigateToRepo(\n        val repoId: Long,\n    ) : AppsAction\n\n    data class OnUninstallApp(\n        val app: InstalledAppUi,\n    ) : AppsAction\n\n    // Uninstall confirmation\n    data class OnUninstallConfirmed(val app: InstalledAppUi) : AppsAction\n    data object OnDismissUninstallDialog : AppsAction\n\n    // Link app to repo\n    data object OnAddByLinkClick : AppsAction\n    data object OnDismissLinkSheet : AppsAction\n    data class OnDeviceAppSearchChange(val query: String) : AppsAction\n    data class OnDeviceAppSelected(val app: DeviceAppUi) : AppsAction\n    data class OnRepoUrlChanged(val url: String) : AppsAction\n    data object OnValidateAndLinkRepo : AppsAction\n    data object OnBackToAppPicker : AppsAction\n    data class OnLinkAssetSelected(val asset: GithubAssetUi) : AppsAction\n    data object OnBackToEnterUrl : AppsAction\n\n    // Export/Import\n    data object OnExportApps : AppsAction\n    data object OnImportApps : AppsAction\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsEvent.kt",
    "content": "package zed.rainxch.apps.presentation\n\nimport zed.rainxch.apps.domain.model.ImportResult\n\nsealed interface AppsEvent {\n    data class ShowError(\n        val message: String,\n    ) : AppsEvent\n\n    data class ShowSuccess(\n        val message: String,\n    ) : AppsEvent\n\n    data class NavigateToRepo(\n        val repoId: Long,\n    ) : AppsEvent\n\n    data class AppLinkedSuccessfully(\n        val appName: String,\n    ) : AppsEvent\n\n    data class ImportComplete(\n        val result: ImportResult,\n    ) : AppsEvent\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n\npackage zed.rainxch.apps.presentation\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.OpenInNew\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Cancel\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.filled.Update\nimport androidx.compose.material.icons.outlined.DeleteOutline\nimport androidx.compose.material.icons.outlined.FileDownload\nimport androidx.compose.material.icons.outlined.FileUpload\nimport androidx.compose.material.icons.outlined.MoreVert\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LinearWavyProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.coil3.CoilImage\nimport io.github.fletchmckee.liquid.liquefiable\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.apps.presentation.components.LinkAppBottomSheet\nimport zed.rainxch.apps.presentation.model.AppItem\nimport zed.rainxch.apps.presentation.model.UpdateAllProgress\nimport zed.rainxch.apps.presentation.model.UpdateState\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.add_by_link\nimport zed.rainxch.githubstore.core.presentation.res.cancel\nimport zed.rainxch.githubstore.core.presentation.res.check_for_updates\nimport zed.rainxch.githubstore.core.presentation.res.checking\nimport zed.rainxch.githubstore.core.presentation.res.checking_for_updates\nimport zed.rainxch.githubstore.core.presentation.res.confirm_uninstall_message\nimport zed.rainxch.githubstore.core.presentation.res.confirm_uninstall_title\nimport zed.rainxch.githubstore.core.presentation.res.currently_updating\nimport zed.rainxch.githubstore.core.presentation.res.downloading\nimport zed.rainxch.githubstore.core.presentation.res.error_with_message\nimport zed.rainxch.githubstore.core.presentation.res.export_apps\nimport zed.rainxch.githubstore.core.presentation.res.import_apps\nimport zed.rainxch.githubstore.core.presentation.res.installed_apps\nimport zed.rainxch.githubstore.core.presentation.res.installing\nimport zed.rainxch.githubstore.core.presentation.res.last_checked\nimport zed.rainxch.githubstore.core.presentation.res.last_checked_hours_ago\nimport zed.rainxch.githubstore.core.presentation.res.last_checked_just_now\nimport zed.rainxch.githubstore.core.presentation.res.last_checked_minutes_ago\nimport zed.rainxch.githubstore.core.presentation.res.no_apps_found\nimport zed.rainxch.githubstore.core.presentation.res.open\nimport zed.rainxch.githubstore.core.presentation.res.pending_install\nimport zed.rainxch.githubstore.core.presentation.res.search_your_apps\nimport zed.rainxch.githubstore.core.presentation.res.uninstall\nimport zed.rainxch.githubstore.core.presentation.res.update\nimport zed.rainxch.githubstore.core.presentation.res.update_all\nimport zed.rainxch.githubstore.core.presentation.res.updated_successfully\nimport zed.rainxch.githubstore.core.presentation.res.updating_x_of_y\n\n@Composable\nfun AppsRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToRepo: (repoId: Long) -> Unit,\n    viewModel: AppsViewModel = koinViewModel(),\n    state: AppsState,\n) {\n    val snackbarHostState = remember { SnackbarHostState() }\n    val coroutineScope = rememberCoroutineScope()\n\n    ObserveAsEvents(viewModel.events) { event ->\n        when (event) {\n            is AppsEvent.NavigateToRepo -> {\n                onNavigateToRepo(event.repoId)\n            }\n\n            is AppsEvent.ShowError -> {\n                coroutineScope.launch {\n                    snackbarHostState.showSnackbar(event.message)\n                }\n            }\n\n            is AppsEvent.ShowSuccess -> {\n                coroutineScope.launch {\n                    snackbarHostState.showSnackbar(event.message)\n                }\n            }\n\n            is AppsEvent.AppLinkedSuccessfully -> { // handled by ShowSuccess\n            }\n\n            is AppsEvent.ImportComplete -> { // handled by ShowSuccess\n            }\n        }\n    }\n\n    AppsScreen(\n        state = state,\n        onAction = { action ->\n            when (action) {\n                AppsAction.OnNavigateBackClick -> {\n                    onNavigateBack()\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n        snackbarHostState = snackbarHostState,\n    )\n}\n\n@Composable\nfun AppsScreen(\n    state: AppsState,\n    onAction: (AppsAction) -> Unit,\n    snackbarHostState: SnackbarHostState,\n) {\n    val liquidState = LocalBottomNavigationLiquid.current\n    val bottomNavHeight = LocalBottomNavigationHeight.current\n    var showOverflowMenu by remember { mutableStateOf(false) }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = {\n                    Text(\n                        text = stringResource(Res.string.installed_apps),\n                        style = MaterialTheme.typography.titleMediumEmphasized,\n                        fontWeight = FontWeight.SemiBold,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                },\n                actions = {\n                    IconButton(\n                        onClick = { onAction(AppsAction.OnCheckAllForUpdates) },\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Refresh,\n                            contentDescription = stringResource(Res.string.check_for_updates),\n                        )\n                    }\n\n                    Box {\n                        IconButton(onClick = { showOverflowMenu = true }) {\n                            Icon(\n                                imageVector = Icons.Outlined.MoreVert,\n                                contentDescription = null,\n                            )\n                        }\n                        DropdownMenu(\n                            expanded = showOverflowMenu,\n                            onDismissRequest = { showOverflowMenu = false },\n                        ) {\n                            DropdownMenuItem(\n                                text = { Text(stringResource(Res.string.export_apps)) },\n                                onClick = {\n                                    showOverflowMenu = false\n                                    onAction(AppsAction.OnExportApps)\n                                },\n                                leadingIcon = {\n                                    Icon(Icons.Outlined.FileUpload, contentDescription = null)\n                                },\n                            )\n                            DropdownMenuItem(\n                                text = { Text(stringResource(Res.string.import_apps)) },\n                                onClick = {\n                                    showOverflowMenu = false\n                                    onAction(AppsAction.OnImportApps)\n                                },\n                                leadingIcon = {\n                                    Icon(Icons.Outlined.FileDownload, contentDescription = null)\n                                },\n                            )\n                        }\n                    }\n                },\n            )\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                onClick = { onAction(AppsAction.OnAddByLinkClick) },\n                modifier = Modifier.padding(bottom = bottomNavHeight),\n            ) {\n                Icon(\n                    imageVector = Icons.Default.Add,\n                    contentDescription = stringResource(Res.string.add_by_link),\n                )\n            }\n        },\n        snackbarHost = {\n            SnackbarHost(\n                hostState = snackbarHostState,\n                modifier = Modifier.padding(bottomNavHeight + 16.dp),\n            )\n        },\n        modifier =\n            Modifier\n                .then(\n                    if (state.isLiquidGlassEnabled) {\n                        Modifier.liquefiable(liquidState)\n                    } else {\n                        Modifier\n                    },\n                ),\n    ) { innerPadding ->\n\n        // Link app bottom sheet\n        if (state.showLinkSheet) {\n            LinkAppBottomSheet(\n                state = state,\n                onAction = onAction,\n            )\n        }\n\n        // Uninstall confirmation dialog\n        state.appPendingUninstall?.let { app ->\n            AlertDialog(\n                onDismissRequest = { onAction(AppsAction.OnDismissUninstallDialog) },\n                title = {\n                    Text(\n                        text = stringResource(Res.string.confirm_uninstall_title),\n                        fontWeight = FontWeight.Bold,\n                    )\n                },\n                text = {\n                    Text(\n                        text = stringResource(Res.string.confirm_uninstall_message, app.appName),\n                    )\n                },\n                confirmButton = {\n                    TextButton(\n                        onClick = { onAction(AppsAction.OnUninstallConfirmed(app)) },\n                    ) {\n                        Text(\n                            text = stringResource(Res.string.uninstall),\n                            color = MaterialTheme.colorScheme.error,\n                        )\n                    }\n                },\n                dismissButton = {\n                    TextButton(\n                        onClick = { onAction(AppsAction.OnDismissUninstallDialog) },\n                    ) {\n                        Text(text = stringResource(Res.string.cancel))\n                    }\n                },\n            )\n        }\n\n        PullToRefreshBox(\n            isRefreshing = state.isRefreshing,\n            onRefresh = { onAction(AppsAction.OnRefresh) },\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n        ) {\n            Column(\n                modifier = Modifier.fillMaxSize(),\n            ) {\n                TextField(\n                    value = state.searchQuery,\n                    onValueChange = { onAction(AppsAction.OnSearchChange(it)) },\n                    leadingIcon = {\n                        Icon(Icons.Default.Search, contentDescription = null)\n                    },\n                    placeholder = { Text(stringResource(Res.string.search_your_apps)) },\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 16.dp, vertical = 8.dp),\n                    shape = CircleShape,\n                    colors =\n                        TextFieldDefaults.colors(\n                            focusedIndicatorColor = Color.Transparent,\n                            unfocusedIndicatorColor = Color.Transparent,\n                        ),\n                )\n\n                if (state.isCheckingForUpdates) {\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp, vertical = 4.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    ) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(14.dp),\n                            strokeWidth = 2.dp,\n                        )\n                        Text(\n                            text = stringResource(Res.string.checking_for_updates),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                } else if (state.lastCheckedTimestamp != null) {\n                    Text(\n                        text =\n                            stringResource(\n                                Res.string.last_checked,\n                                formatLastChecked(state.lastCheckedTimestamp),\n                            ),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.outline,\n                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),\n                    )\n                }\n\n                val hasUpdates = state.apps.any { it.installedApp.isUpdateAvailable }\n                if (hasUpdates && !state.isUpdatingAll) {\n                    Button(\n                        onClick = { onAction(AppsAction.OnUpdateAll) },\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp, vertical = 8.dp),\n                        enabled = state.updateAllButtonEnabled,\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Update,\n                            contentDescription = null,\n                        )\n\n                        Spacer(Modifier.width(8.dp))\n\n                        Text(\n                            text = stringResource(Res.string.update_all),\n                        )\n                    }\n                }\n\n                if (state.isUpdatingAll && state.updateAllProgress != null) {\n                    UpdateAllProgressCard(\n                        progress = state.updateAllProgress,\n                        onCancel = {\n                            onAction(AppsAction.OnCancelUpdateAll)\n                        },\n                    )\n                }\n\n                when {\n                    state.isLoading -> {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            CircularProgressIndicator()\n                        }\n                    }\n\n                    state.filteredApps.isEmpty() -> {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            Text(\n                                text = stringResource(Res.string.no_apps_found),\n                                style = MaterialTheme.typography.titleMedium,\n                                color = MaterialTheme.colorScheme.onBackground,\n                            )\n                        }\n                    }\n\n                    else -> {\n                        LazyColumn(\n                            modifier = Modifier.fillMaxSize(),\n                            contentPadding = PaddingValues(16.dp),\n                            verticalArrangement = Arrangement.spacedBy(12.dp),\n                        ) {\n                            items(\n                                items = state.filteredApps,\n                                key = { it.installedApp.packageName },\n                            ) { appItem ->\n                                AppItemCard(\n                                    appItem = appItem,\n                                    onOpenClick = { onAction(AppsAction.OnOpenApp(appItem.installedApp)) },\n                                    onUpdateClick = { onAction(AppsAction.OnUpdateApp(appItem.installedApp)) },\n                                    onCancelClick = { onAction(AppsAction.OnCancelUpdate(appItem.installedApp.packageName)) },\n                                    onUninstallClick = { onAction(AppsAction.OnUninstallApp(appItem.installedApp)) },\n                                    onRepoClick = { onAction(AppsAction.OnNavigateToRepo(appItem.installedApp.repoId)) },\n                                    modifier =\n                                        Modifier\n                                            .then(\n                                                if (state.isLiquidGlassEnabled) {\n                                                    Modifier.liquefiable(liquidState)\n                                                } else {\n                                                    Modifier\n                                                },\n                                            ),\n                                )\n                            }\n\n                            item {\n                                Spacer(Modifier.height(bottomNavHeight + 32.dp))\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun UpdateAllProgressCard(\n    progress: UpdateAllProgress,\n    onCancel: () -> Unit,\n) {\n    Card(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp, vertical = 8.dp),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Text(\n                    text =\n                        stringResource(\n                            Res.string.updating_x_of_y,\n                            progress.current,\n                            progress.total,\n                        ),\n                    style = MaterialTheme.typography.titleMedium,\n                )\n                IconButton(onClick = onCancel) {\n                    Icon(\n                        Icons.Default.Close,\n                        contentDescription = stringResource(Res.string.cancel),\n                    )\n                }\n            }\n\n            Spacer(Modifier.height(8.dp))\n\n            Text(\n                text =\n                    stringResource(\n                        Res.string.currently_updating,\n                        progress.currentAppName,\n                    ),\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n\n            Spacer(Modifier.height(12.dp))\n\n            LinearWavyProgressIndicator(\n                progress = { progress.current.toFloat() / progress.total },\n                modifier = Modifier.fillMaxWidth(),\n            )\n        }\n    }\n}\n\n@Composable\nfun AppItemCard(\n    appItem: AppItem,\n    onOpenClick: () -> Unit,\n    onUpdateClick: () -> Unit,\n    onCancelClick: () -> Unit,\n    onUninstallClick: () -> Unit,\n    onRepoClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    val app = appItem.installedApp\n\n    ExpressiveCard(\n        onClick = onRepoClick,\n        modifier = modifier,\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .clip(RoundedCornerShape(32.dp))\n                    .padding(16.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                CoilImage(\n                    imageModel = { app.repoOwnerAvatarUrl },\n                    modifier =\n                        Modifier\n                            .size(64.dp)\n                            .clip(CircleShape),\n                    loading = {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            CircularWavyProgressIndicator()\n                        }\n                    },\n                )\n\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = app.appName,\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        fontWeight = FontWeight.Bold,\n                    )\n\n                    Text(\n                        text = app.repoOwner,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n\n                    when {\n                        app.isPendingInstall -> {\n                            Text(\n                                text = stringResource(Res.string.pending_install),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.tertiary,\n                            )\n                        }\n\n                        app.isUpdateAvailable -> {\n                            Text(\n                                text = \"${app.installedVersion} → ${app.latestVersion}\",\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.primary,\n                            )\n                        }\n\n                        else -> {\n                            Text(\n                                text = app.installedVersion,\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n            }\n\n            if (app.repoDescription != null) {\n                Spacer(Modifier.height(8.dp))\n\n                Text(\n                    text = app.repoDescription,\n                    style = MaterialTheme.typography.bodyMediumEmphasized,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n\n            Spacer(Modifier.height(12.dp))\n\n            when (val state = appItem.updateState) {\n                is UpdateState.Downloading -> {\n                    Column {\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                        ) {\n                            Text(\n                                text = stringResource(Res.string.downloading),\n                                style = MaterialTheme.typography.bodySmall,\n                            )\n                            if (appItem.downloadProgress != null) {\n                                Text(\n                                    text = \"${appItem.downloadProgress}%\",\n                                    style = MaterialTheme.typography.bodySmall,\n                                )\n                            }\n                        }\n                        Spacer(Modifier.height(4.dp))\n                        LinearWavyProgressIndicator(\n                            progress = { (appItem.downloadProgress ?: 0) / 100f },\n                            modifier = Modifier.fillMaxWidth(),\n                        )\n                    }\n                }\n\n                is UpdateState.Installing -> {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    ) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(16.dp),\n                            strokeWidth = 2.dp,\n                        )\n                        Text(\n                            text = stringResource(Res.string.installing),\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    }\n                }\n\n                is UpdateState.CheckingUpdate -> {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    ) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(16.dp),\n                            strokeWidth = 2.dp,\n                        )\n                        Text(\n                            text = stringResource(Res.string.checking),\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    }\n                }\n\n                is UpdateState.Success -> {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.CheckCircle,\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.size(16.dp),\n                        )\n                        Text(\n                            text = stringResource(Res.string.updated_successfully),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                    }\n                }\n\n                is UpdateState.Error -> {\n                    Text(\n                        text = stringResource(Res.string.error_with_message, state.message),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.error,\n                    )\n                }\n\n                UpdateState.Idle -> {}\n            }\n\n            Spacer(Modifier.height(12.dp))\n\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                if (!app.isPendingInstall &&\n                    appItem.updateState !is UpdateState.Downloading &&\n                    appItem.updateState !is UpdateState.Installing &&\n                    appItem.updateState !is UpdateState.CheckingUpdate\n                ) {\n                    IconButton(\n                        onClick = onUninstallClick,\n                    ) {\n                        Icon(\n                            imageVector = Icons.Outlined.DeleteOutline,\n                            contentDescription = stringResource(Res.string.uninstall),\n                            tint = MaterialTheme.colorScheme.error,\n                        )\n                    }\n                }\n\n                Button(\n                    shapes = ButtonDefaults.shapes(),\n                    onClick = onOpenClick,\n                    modifier = Modifier.weight(1f),\n                    enabled =\n                        !app.isPendingInstall &&\n                            appItem.updateState !is UpdateState.Downloading &&\n                            appItem.updateState !is UpdateState.Installing,\n                ) {\n                    Icon(\n                        imageVector = Icons.AutoMirrored.Filled.OpenInNew,\n                        contentDescription = null,\n                        modifier = Modifier.size(18.dp),\n                    )\n                    Spacer(Modifier.width(4.dp))\n                    Text(\n                        text = stringResource(Res.string.open),\n                    )\n                }\n\n                when (appItem.updateState) {\n                    is UpdateState.Downloading, is UpdateState.Installing, is UpdateState.CheckingUpdate -> {\n                        Button(\n                            onClick = onCancelClick,\n                            modifier = Modifier.weight(1f),\n                            colors =\n                                ButtonDefaults.buttonColors(\n                                    containerColor = MaterialTheme.colorScheme.errorContainer,\n                                    contentColor = MaterialTheme.colorScheme.onErrorContainer,\n                                ),\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.Cancel,\n                                contentDescription = stringResource(Res.string.cancel),\n                                modifier = Modifier.size(18.dp),\n                            )\n\n                            Spacer(Modifier.width(4.dp))\n\n                            Text(\n                                text = stringResource(Res.string.cancel),\n                            )\n                        }\n                    }\n\n                    else -> {\n                        if (app.isUpdateAvailable && !app.isPendingInstall) {\n                            Button(\n                                onClick = onUpdateClick,\n                                modifier = Modifier.weight(1f),\n                            ) {\n                                Icon(\n                                    imageVector = Icons.Default.Update,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                )\n                                Spacer(Modifier.width(4.dp))\n                                Text(\n                                    text = stringResource(Res.string.update),\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun formatLastChecked(timestamp: Long): String {\n    val now = System.currentTimeMillis()\n    val diff = now - timestamp\n    val minutes = diff / (60 * 1000)\n    val hours = diff / (60 * 60 * 1000)\n\n    return when {\n        minutes < 1 -> stringResource(Res.string.last_checked_just_now)\n        minutes < 60 -> stringResource(Res.string.last_checked_minutes_ago, minutes.toInt())\n        else -> stringResource(Res.string.last_checked_hours_ago, hours.toInt())\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        AppsScreen(\n            state = AppsState(),\n            onAction = {},\n            snackbarHostState = SnackbarHostState(),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt",
    "content": "package zed.rainxch.apps.presentation\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport kotlinx.collections.immutable.toImmutableList\nimport zed.rainxch.apps.domain.model.GithubRepoInfo\nimport zed.rainxch.apps.presentation.model.AppItem\nimport zed.rainxch.apps.presentation.model.DeviceAppUi\nimport zed.rainxch.apps.presentation.model.GithubAssetUi\nimport zed.rainxch.apps.presentation.model.GithubRepoInfoUi\nimport zed.rainxch.apps.presentation.model.InstalledAppUi\nimport zed.rainxch.apps.presentation.model.UpdateAllProgress\nimport zed.rainxch.core.domain.model.DeviceApp\nimport zed.rainxch.core.domain.model.GithubAsset\n\ndata class AppsState(\n    val apps: ImmutableList<AppItem> = persistentListOf(),\n    val filteredApps: ImmutableList<AppItem> = persistentListOf(),\n    val searchQuery: String = \"\",\n    val isLoading: Boolean = false,\n    val isUpdatingAll: Boolean = false,\n    val updateAllProgress: UpdateAllProgress? = null,\n    val updateAllButtonEnabled: Boolean = true,\n    val isCheckingForUpdates: Boolean = false,\n    val lastCheckedTimestamp: Long? = null,\n    val isRefreshing: Boolean = false,\n    val isLiquidGlassEnabled: Boolean = true,\n    // Link app to repo\n    val showLinkSheet: Boolean = false,\n    val linkStep: LinkStep = LinkStep.PickApp,\n    val deviceApps: ImmutableList<DeviceAppUi> = persistentListOf(),\n    val deviceAppSearchQuery: String = \"\",\n    val selectedDeviceApp: DeviceAppUi? = null,\n    val repoUrl: String = \"\",\n    val isValidatingRepo: Boolean = false,\n    val repoValidationError: String? = null,\n    val linkValidationStatus: String? = null,\n    val linkInstallableAssets: ImmutableList<GithubAssetUi> = persistentListOf(),\n    val linkSelectedAsset: GithubAssetUi? = null,\n    val linkDownloadProgress: Int? = null,\n    val fetchedRepoInfo: GithubRepoInfoUi? = null,\n    // Export/Import\n    val isExporting: Boolean = false,\n    val isImporting: Boolean = false,\n    // Uninstall confirmation\n    val appPendingUninstall: InstalledAppUi? = null,\n) {\n    val filteredDeviceApps: ImmutableList<DeviceAppUi>\n        get() =\n            if (deviceAppSearchQuery.isBlank()) {\n                deviceApps.toImmutableList()\n            } else {\n                deviceApps\n                    .filter {\n                        it.appName.contains(deviceAppSearchQuery, ignoreCase = true) ||\n                            it.packageName.contains(deviceAppSearchQuery, ignoreCase = true)\n                    }.toImmutableList()\n            }\n}\n\nenum class LinkStep {\n    PickApp,\n    EnterUrl,\n    PickAsset,\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt",
    "content": "package zed.rainxch.apps.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport kotlinx.collections.immutable.toImmutableList\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.apps.domain.repository.AppsRepository\nimport zed.rainxch.apps.presentation.mappers.toDomain\nimport zed.rainxch.apps.presentation.mappers.toUi\nimport zed.rainxch.apps.presentation.model.AppItem\nimport zed.rainxch.apps.presentation.model.GithubAssetUi\nimport zed.rainxch.apps.presentation.model.InstalledAppUi\nimport zed.rainxch.apps.presentation.model.UpdateAllProgress\nimport zed.rainxch.apps.presentation.model.UpdateState\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.network.Downloader\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\nimport zed.rainxch.core.domain.utils.ShareManager\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport java.io.File\n\nclass AppsViewModel(\n    private val appsRepository: AppsRepository,\n    private val installer: Installer,\n    private val downloader: Downloader,\n    private val installedAppsRepository: InstalledAppsRepository,\n    private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase,\n    private val logger: GitHubStoreLogger,\n    private val shareManager: ShareManager,\n    private val tweaksRepository: TweaksRepository,\n) : ViewModel() {\n    companion object {\n        private const val UPDATE_CHECK_COOLDOWN_MS = 30 * 60 * 1000L\n    }\n\n    private var hasLoadedInitialData = false\n    private val activeUpdates = mutableMapOf<String, Job>()\n    private var updateAllJob: Job? = null\n    private var lastAutoCheckTimestamp: Long = 0L\n\n    private val _state = MutableStateFlow(AppsState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    loadApps()\n                    observeLiquidGlassEnabled()\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = AppsState(),\n            )\n\n    private fun observeLiquidGlassEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getLiquidGlassEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(\n                        isLiquidGlassEnabled = enabled,\n                    )\n                }\n            }\n        }\n    }\n\n    private val _events = Channel<AppsEvent>()\n    val events = _events.receiveAsFlow()\n\n    private fun loadApps() {\n        viewModelScope.launch {\n            _state.update { it.copy(isLoading = true) }\n\n            try {\n                val syncResult = syncInstalledAppsUseCase()\n                if (syncResult.isFailure) {\n                    logger.error(\"Sync had issues but continuing: ${syncResult.exceptionOrNull()?.message}\")\n                }\n\n                appsRepository.getApps().collect { apps ->\n                    val appItems =\n                        apps\n                            .map { it.toUi() }\n                            .map { app ->\n                                val existing =\n                                    _state.value.apps.find {\n                                        it.installedApp.packageName == app.packageName\n                                    }\n                                AppItem(\n                                    installedApp = app,\n                                    updateState = existing?.updateState ?: UpdateState.Idle,\n                                    downloadProgress = existing?.downloadProgress,\n                                    error = existing?.error,\n                                )\n                            }.sortedByDescending { it.installedApp.isUpdateAvailable }\n                            .toImmutableList()\n\n                    _state.update {\n                        it.copy(\n                            apps = appItems,\n                            isLoading = false,\n                            updateAllButtonEnabled =\n                                appItems.any { item ->\n                                    item.installedApp.isUpdateAvailable\n                                },\n                        )\n                    }\n\n                    filterApps()\n                }\n            } catch (e: Exception) {\n                logger.error(\"Failed to load apps: ${e.message}\")\n                _state.update {\n                    it.copy(isLoading = false)\n                }\n            }\n\n            autoCheckForUpdatesIfNeeded()\n        }\n    }\n\n    private fun autoCheckForUpdatesIfNeeded() {\n        val now = System.currentTimeMillis()\n        if (now - lastAutoCheckTimestamp < UPDATE_CHECK_COOLDOWN_MS) {\n            logger.debug(\"Skipping auto-check: last check was ${(now - lastAutoCheckTimestamp) / 1000}s ago\")\n            return\n        }\n        checkAllForUpdates()\n    }\n\n    private fun checkAllForUpdates() {\n        viewModelScope.launch {\n            _state.update { it.copy(isCheckingForUpdates = true) }\n            try {\n                syncInstalledAppsUseCase()\n                installedAppsRepository.checkAllForUpdates()\n                val now = System.currentTimeMillis()\n                lastAutoCheckTimestamp = now\n                _state.update { it.copy(lastCheckedTimestamp = now) }\n            } catch (e: Exception) {\n                logger.error(\"Check all for updates failed: ${e.message}\")\n            } finally {\n                _state.update { it.copy(isCheckingForUpdates = false) }\n            }\n        }\n    }\n\n    private fun refresh() {\n        viewModelScope.launch {\n            _state.update { it.copy(isRefreshing = true) }\n            try {\n                syncInstalledAppsUseCase()\n                installedAppsRepository.checkAllForUpdates()\n                val now = System.currentTimeMillis()\n                lastAutoCheckTimestamp = now\n                _state.update { it.copy(lastCheckedTimestamp = now) }\n            } catch (e: Exception) {\n                logger.error(\"Refresh failed: ${e.message}\")\n            } finally {\n                _state.update { it.copy(isRefreshing = false) }\n            }\n        }\n    }\n\n    fun onAction(action: AppsAction) {\n        when (action) {\n            AppsAction.OnNavigateBackClick -> {\n            }\n\n            is AppsAction.OnSearchChange -> {\n                _state.update {\n                    it.copy(searchQuery = action.query)\n                }\n\n                filterApps()\n            }\n\n            is AppsAction.OnOpenApp -> {\n                openApp(action.app)\n            }\n\n            is AppsAction.OnUpdateApp -> {\n                updateSingleApp(action.app)\n            }\n\n            is AppsAction.OnCancelUpdate -> {\n                cancelUpdate(action.packageName)\n            }\n\n            AppsAction.OnUpdateAll -> {\n                updateAllApps()\n            }\n\n            AppsAction.OnCancelUpdateAll -> {\n                cancelAllUpdates()\n            }\n\n            AppsAction.OnCheckAllForUpdates -> {\n                checkAllForUpdates()\n            }\n\n            AppsAction.OnRefresh -> {\n                refresh()\n            }\n\n            is AppsAction.OnNavigateToRepo -> {\n                viewModelScope.launch {\n                    _events.send(AppsEvent.NavigateToRepo(action.repoId))\n                }\n            }\n\n            is AppsAction.OnUninstallApp -> {\n                _state.update { it.copy(appPendingUninstall = action.app) }\n            }\n\n            AppsAction.OnAddByLinkClick -> {\n                openLinkSheet()\n            }\n\n            AppsAction.OnDismissLinkSheet -> {\n                dismissLinkSheet()\n            }\n\n            is AppsAction.OnDeviceAppSearchChange -> {\n                _state.update { it.copy(deviceAppSearchQuery = action.query) }\n            }\n\n            is AppsAction.OnDeviceAppSelected -> {\n                _state.update {\n                    it.copy(\n                        selectedDeviceApp = action.app,\n                        linkStep = LinkStep.EnterUrl,\n                        repoUrl = \"\",\n                        repoValidationError = null,\n                        fetchedRepoInfo = null,\n                    )\n                }\n            }\n\n            is AppsAction.OnRepoUrlChanged -> {\n                _state.update {\n                    it.copy(\n                        repoUrl = action.url,\n                        repoValidationError = null,\n                    )\n                }\n            }\n\n            AppsAction.OnValidateAndLinkRepo -> {\n                validateAndLinkRepo()\n            }\n\n            AppsAction.OnBackToAppPicker -> {\n                _state.update {\n                    it.copy(\n                        linkStep = LinkStep.PickApp,\n                        selectedDeviceApp = null,\n                        repoUrl = \"\",\n                        repoValidationError = null,\n                        fetchedRepoInfo = null,\n                        linkInstallableAssets = persistentListOf(),\n                        linkSelectedAsset = null,\n                        linkDownloadProgress = null,\n                    )\n                }\n            }\n\n            is AppsAction.OnLinkAssetSelected -> {\n                validateWithAsset(action.asset)\n            }\n\n            AppsAction.OnBackToEnterUrl -> {\n                _state.update {\n                    it.copy(\n                        linkStep = LinkStep.EnterUrl,\n                        linkInstallableAssets = persistentListOf(),\n                        linkSelectedAsset = null,\n                        linkDownloadProgress = null,\n                        linkValidationStatus = null,\n                        repoValidationError = null,\n                    )\n                }\n            }\n\n            AppsAction.OnExportApps -> {\n                exportApps()\n            }\n\n            AppsAction.OnImportApps -> {\n                importAppsFromFile()\n            }\n\n            is AppsAction.OnUninstallConfirmed -> {\n                uninstallApp(action.app)\n                _state.update { it.copy(appPendingUninstall = null) }\n            }\n\n            AppsAction.OnDismissUninstallDialog -> {\n                _state.update { it.copy(appPendingUninstall = null) }\n            }\n        }\n    }\n\n    private fun filterApps() {\n        _state.update { current ->\n            current.copy(\n                filteredApps = computeFilteredApps(current.apps, current.searchQuery),\n            )\n        }\n    }\n\n    private fun computeFilteredApps(\n        apps: ImmutableList<AppItem>,\n        query: String,\n    ): ImmutableList<AppItem> =\n        if (query.isBlank()) {\n            apps\n                .sortedBy { it.installedApp.isUpdateAvailable }\n                .toImmutableList()\n        } else {\n            apps\n                .filter { appItem ->\n                    appItem.installedApp.appName.contains(query, ignoreCase = true) ||\n                        appItem.installedApp.repoOwner.contains(query, ignoreCase = true)\n                }.sortedBy { it.installedApp.isUpdateAvailable }\n                .toImmutableList()\n        }\n\n    private fun uninstallApp(app: InstalledAppUi) {\n        viewModelScope.launch {\n            try {\n                installer.uninstall(app.packageName)\n                logger.debug(\"Requested uninstall for ${app.packageName}\")\n            } catch (e: Exception) {\n                logger.error(\"Failed to request uninstall for ${app.packageName}: ${e.message}\")\n                _events.send(\n                    AppsEvent.ShowError(\n                        getString(Res.string.failed_to_uninstall, app.appName),\n                    ),\n                )\n            }\n        }\n    }\n\n    private fun openApp(app: InstalledAppUi) {\n        viewModelScope.launch {\n            try {\n                appsRepository.openApp(\n                    installedApp = app.toDomain(),\n                    onCantLaunchApp = {\n                        viewModelScope.launch {\n                            _events.send(\n                                AppsEvent.ShowError(\n                                    getString(\n                                        Res.string.cannot_launch,\n                                        app.appName,\n                                    ),\n                                ),\n                            )\n                        }\n                    },\n                )\n            } catch (e: Exception) {\n                logger.error(\"Failed to open app: ${e.message}\")\n                _events.send(\n                    AppsEvent.ShowError(\n                        getString(\n                            Res.string.failed_to_open,\n                            app.appName,\n                        ),\n                    ),\n                )\n            }\n        }\n    }\n\n    private fun updateSingleApp(app: InstalledAppUi) {\n        if (activeUpdates.containsKey(app.packageName)) {\n            logger.debug(\"Update already in progress for ${app.packageName}\")\n            return\n        }\n\n        val job =\n            viewModelScope.launch {\n                try {\n                    updateAppState(app.packageName, UpdateState.CheckingUpdate)\n\n                    val latestRelease =\n                        try {\n                            appsRepository.getLatestRelease(\n                                owner = app.repoOwner,\n                                repo = app.repoName,\n                            )\n                        } catch (e: Exception) {\n                            logger.error(\"Failed to fetch latest release: ${e.message}\")\n                            throw IllegalStateException(\"Failed to fetch latest release: ${e.message}\")\n                        }\n\n                    if (latestRelease == null) {\n                        throw IllegalStateException(\"No release found for ${app.appName}\")\n                    }\n\n                    val installableAssets =\n                        latestRelease.assets.filter { asset ->\n                            installer.isAssetInstallable(asset.name)\n                        }\n\n                    if (installableAssets.isEmpty()) {\n                        throw IllegalStateException(\"No installable assets found for this platform\")\n                    }\n\n                    val primaryAsset =\n                        installer.choosePrimaryAsset(installableAssets)\n                            ?: throw IllegalStateException(\"Could not determine primary asset\")\n\n                    logger.debug(\n                        \"Update: ${app.appName} from ${app.installedVersion} to ${latestRelease.tagName}, \" +\n                            \"asset: ${primaryAsset.name}\",\n                    )\n\n                    val latestAssetUrl = primaryAsset.downloadUrl\n                    val latestAssetName = primaryAsset.name\n                    val latestVersion = latestRelease.tagName\n\n                    val ext = latestAssetName.substringAfterLast('.', \"\").lowercase()\n                    installer.ensurePermissionsOrThrow(ext)\n\n                    val existingPath = downloader.getDownloadedFilePath(latestAssetName)\n                    if (existingPath != null) {\n                        val file = File(existingPath)\n                        try {\n                            val apkInfo =\n                                installer.getApkInfoExtractor().extractPackageInfo(existingPath)\n                            val normalizedExisting =\n                                apkInfo?.versionName?.removePrefix(\"v\")?.removePrefix(\"V\") ?: \"\"\n                            val normalizedLatest =\n                                latestVersion.removePrefix(\"v\").removePrefix(\"V\")\n                            if (normalizedExisting != normalizedLatest) {\n                                val deleted = file.delete()\n                                logger.debug(\"Deleted mismatched existing file ($normalizedExisting != $normalizedLatest): $deleted\")\n                            }\n                        } catch (e: Exception) {\n                            logger.debug(\"Failed to extract APK info for existing file: ${e.message}\")\n                            val deleted = file.delete()\n                            logger.debug(\"Deleted unextractable existing file: $deleted\")\n                        }\n                    }\n\n                    updateAppState(app.packageName, UpdateState.Downloading)\n\n                    downloader.download(latestAssetUrl, latestAssetName).collect { progress ->\n                        updateAppProgress(app.packageName, progress.percent)\n                    }\n\n                    val filePath =\n                        downloader.getDownloadedFilePath(latestAssetName)\n                            ?: throw IllegalStateException(\"Downloaded file not found\")\n\n                    val apkInfo =\n                        installer.getApkInfoExtractor().extractPackageInfo(filePath)\n                            ?: throw IllegalStateException(\"Failed to extract APK info\")\n\n                    val currentApp = installedAppsRepository.getAppByPackage(app.packageName)\n                    if (currentApp != null) {\n                        installedAppsRepository.updateApp(\n                            currentApp.copy(\n                                isPendingInstall = true,\n                                latestVersion = latestVersion,\n                                latestAssetName = latestAssetName,\n                                latestAssetUrl = latestAssetUrl,\n                                latestVersionName = apkInfo.versionName,\n                                latestVersionCode = apkInfo.versionCode,\n                            ),\n                        )\n                    } else {\n                        markPendingUpdate(app.toDomain())\n                    }\n\n                    updateAppState(app.packageName, UpdateState.Installing)\n\n                    try {\n                        installer.install(filePath, ext)\n                    } catch (e: Exception) {\n                        installedAppsRepository.updatePendingStatus(app.packageName, false)\n                        throw e\n                    }\n\n                    updateAppState(app.packageName, UpdateState.Idle)\n\n                    logger.debug(\"Launched installer for ${app.appName} $latestVersion, waiting for system confirmation\")\n                } catch (e: CancellationException) {\n                    logger.debug(\"Update cancelled for ${app.packageName}\")\n                    cleanupUpdate(app.packageName, app.latestAssetName)\n                    try {\n                        installedAppsRepository.updatePendingStatus(app.packageName, false)\n                    } catch (clearEx: Exception) {\n                        logger.error(\"Failed to clear pending status on cancellation: ${clearEx.message}\")\n                    }\n                    updateAppState(app.packageName, UpdateState.Idle)\n                    throw e\n                } catch (_: RateLimitException) {\n                    logger.debug(\"Rate limited during update for ${app.packageName}\")\n                    try {\n                        installedAppsRepository.updatePendingStatus(app.packageName, false)\n                    } catch (clearEx: Exception) {\n                        logger.error(\"Failed to clear pending status on rate limit: ${clearEx.message}\")\n                    }\n                    updateAppState(app.packageName, UpdateState.Idle)\n                    _events.send(\n                        AppsEvent.ShowError(getString(Res.string.rate_limit_exceeded)),\n                    )\n                } catch (e: Exception) {\n                    logger.error(\"Update failed for ${app.packageName}: ${e.message}\")\n                    cleanupUpdate(app.packageName, app.latestAssetName)\n                    try {\n                        installedAppsRepository.updatePendingStatus(app.packageName, false)\n                    } catch (clearEx: Exception) {\n                        logger.error(\"Failed to clear pending status on error: ${clearEx.message}\")\n                    }\n                    updateAppState(\n                        app.packageName,\n                        UpdateState.Error(e.message ?: \"Update failed\"),\n                    )\n                    _events.send(\n                        AppsEvent.ShowError(\n                            getString(\n                                Res.string.failed_to_update,\n                                app.appName,\n                                e.message ?: \"\",\n                            ),\n                        ),\n                    )\n                } finally {\n                    activeUpdates.remove(app.packageName)\n                }\n            }\n\n        activeUpdates[app.packageName] = job\n    }\n\n    private fun updateAllApps() {\n        if (_state.value.isUpdatingAll) {\n            logger.error(\"Update all already in progress\")\n            return\n        }\n\n        updateAllJob =\n            viewModelScope.launch {\n                try {\n                    _state.update { it.copy(isUpdatingAll = true) }\n\n                    val appsToUpdate =\n                        _state.value.apps.filter {\n                            it.installedApp.isUpdateAvailable &&\n                                it.updateState !is UpdateState.Success\n                        }\n\n                    if (appsToUpdate.isEmpty()) {\n                        _events.send(AppsEvent.ShowError(getString(Res.string.no_updates_available)))\n                        return@launch\n                    }\n\n                    logger.debug(\"Starting update all for ${appsToUpdate.size} apps\")\n\n                    appsToUpdate.forEachIndexed { index, appItem ->\n                        if (!isActive) {\n                            logger.debug(\"Update all cancelled\")\n                            return@launch\n                        }\n\n                        _state.update {\n                            it.copy(\n                                updateAllProgress =\n                                    UpdateAllProgress(\n                                        current = index + 1,\n                                        total = appsToUpdate.size,\n                                        currentAppName = appItem.installedApp.appName,\n                                    ),\n                            )\n                        }\n\n                        logger.debug(\"Updating ${index + 1}/${appsToUpdate.size}: ${appItem.installedApp.appName}\")\n\n                        updateSingleApp(appItem.installedApp)\n                        activeUpdates[appItem.installedApp.packageName]?.join()\n\n                        delay(1000)\n                    }\n\n                    logger.debug(\"Update all completed successfully\")\n                    _events.send(AppsEvent.ShowSuccess(getString(Res.string.all_apps_updated_successfully)))\n                } catch (_: CancellationException) {\n                    logger.debug(\"Update all cancelled\")\n                } catch (e: Exception) {\n                    logger.error(\"Update all failed: ${e.message}\")\n                    _events.send(\n                        AppsEvent.ShowError(\n                            getString(\n                                Res.string.update_all_failed,\n                                arrayOf(e.message),\n                            ),\n                        ),\n                    )\n                } finally {\n                    _state.update {\n                        it.copy(\n                            isUpdatingAll = false,\n                            updateAllProgress = null,\n                        )\n                    }\n                    updateAllJob = null\n                }\n            }\n    }\n\n    private fun cancelUpdate(packageName: String) {\n        activeUpdates[packageName]?.cancel()\n        activeUpdates.remove(packageName)\n\n        val app = _state.value.apps.find { it.installedApp.packageName == packageName }\n        app?.installedApp?.latestAssetName?.let { assetName ->\n            viewModelScope.launch {\n                cleanupUpdate(packageName, assetName)\n            }\n        }\n\n        updateAppState(packageName, UpdateState.Idle)\n    }\n\n    private fun cancelAllUpdates() {\n        updateAllJob?.cancel()\n        updateAllJob = null\n\n        activeUpdates.values.forEach { it.cancel() }\n        activeUpdates.clear()\n\n        viewModelScope.launch {\n            _state.value.apps.forEach { appItem ->\n                if (appItem.updateState != UpdateState.Idle &&\n                    appItem.updateState != UpdateState.Success\n                ) {\n                    appItem.installedApp.latestAssetName?.let { assetName ->\n                        cleanupUpdate(appItem.installedApp.packageName, assetName)\n                    }\n                    updateAppState(appItem.installedApp.packageName, UpdateState.Idle)\n                }\n            }\n        }\n\n        _state.update {\n            it.copy(\n                isUpdatingAll = false,\n                updateAllProgress = null,\n            )\n        }\n    }\n\n    private fun updateAppState(\n        packageName: String,\n        state: UpdateState,\n    ) {\n        _state.update { currentState ->\n            currentState.copy(\n                apps =\n                    currentState.apps\n                        .map { appItem ->\n                            if (appItem.installedApp.packageName == packageName) {\n                                appItem.copy(\n                                    updateState = state,\n                                    downloadProgress =\n                                        if (state is UpdateState.Downloading) {\n                                            appItem.downloadProgress\n                                        } else {\n                                            null\n                                        },\n                                    error = if (state is UpdateState.Error) state.message else null,\n                                )\n                            } else {\n                                appItem\n                            }\n                        }.toImmutableList(),\n            )\n        }\n\n        filterApps()\n    }\n\n    private fun updateAppProgress(\n        packageName: String,\n        progress: Int?,\n    ) {\n        _state.update { currentState ->\n            currentState.copy(\n                apps =\n                    currentState.apps\n                        .map { appItem ->\n                            if (appItem.installedApp.packageName == packageName) {\n                                appItem.copy(downloadProgress = progress)\n                            } else {\n                                appItem\n                            }\n                        }.toImmutableList(),\n            )\n        }\n\n        filterApps()\n    }\n\n    private suspend fun markPendingUpdate(app: InstalledApp) {\n        installedAppsRepository.updatePendingStatus(app.packageName, true)\n        logger.debug(\"Marked ${app.packageName} as pending install\")\n    }\n\n    private suspend fun cleanupUpdate(\n        packageName: String,\n        assetName: String?,\n    ) {\n        try {\n            if (assetName != null) {\n                val deleted = downloader.cancelDownload(assetName)\n                logger.debug(\"Cleanup for $packageName - file deleted: $deleted\")\n            }\n        } catch (e: Exception) {\n            logger.error(\"Cleanup failed for $packageName: ${e.message}\")\n        }\n    }\n\n    private fun openLinkSheet() {\n        viewModelScope.launch {\n            _state.update {\n                it.copy(\n                    showLinkSheet = true,\n                    linkStep = LinkStep.PickApp,\n                    deviceApps = persistentListOf(),\n                    deviceAppSearchQuery = \"\",\n                    selectedDeviceApp = null,\n                    repoUrl = \"\",\n                    repoValidationError = null,\n                    fetchedRepoInfo = null,\n                )\n            }\n\n            try {\n                val trackedPackages = appsRepository.getTrackedPackageNames()\n                val deviceApps =\n                    appsRepository\n                        .getDeviceApps()\n                        .filter { it.packageName !in trackedPackages }\n                        .map { it.toUi() }\n                        .toImmutableList()\n\n                _state.update { it.copy(deviceApps = deviceApps) }\n            } catch (e: Exception) {\n                logger.error(\"Failed to load device apps: ${e.message}\")\n                _events.send(AppsEvent.ShowError(getString(Res.string.failed_to_load_apps)))\n            }\n        }\n    }\n\n    private fun dismissLinkSheet() {\n        _state.update {\n            it.copy(\n                showLinkSheet = false,\n                linkStep = LinkStep.PickApp,\n                deviceApps = persistentListOf(),\n                deviceAppSearchQuery = \"\",\n                selectedDeviceApp = null,\n                repoUrl = \"\",\n                repoValidationError = null,\n                linkValidationStatus = null,\n                linkInstallableAssets = persistentListOf(),\n                linkSelectedAsset = null,\n                linkDownloadProgress = null,\n                fetchedRepoInfo = null,\n                isValidatingRepo = false,\n            )\n        }\n    }\n\n    private fun validateAndLinkRepo() {\n        val selectedApp = _state.value.selectedDeviceApp ?: return\n        val url = _state.value.repoUrl.trim()\n\n        val parsed = parseGithubUrl(url)\n\n        viewModelScope.launch {\n            if (parsed == null) {\n                _state.update { it.copy(repoValidationError = getString(Res.string.invalid_github_url)) }\n                return@launch\n            }\n\n            val (owner, repo) = parsed\n            _state.update {\n                it.copy(\n                    isValidatingRepo = true,\n                    repoValidationError = null,\n                    linkValidationStatus = null,\n                )\n            }\n\n            try {\n                _state.update { it.copy(linkValidationStatus = getString(Res.string.validating_repo)) }\n\n                val repoInfo = appsRepository.fetchRepoInfo(owner, repo)\n                if (repoInfo == null) {\n                    _state.update {\n                        it.copy(\n                            isValidatingRepo = false,\n                            linkValidationStatus = null,\n                            repoValidationError = getString(Res.string.repo_not_found, owner, repo),\n                        )\n                    }\n                    return@launch\n                }\n\n                _state.update {\n                    it.copy(\n                        fetchedRepoInfo = repoInfo.toUi(),\n                        linkValidationStatus = getString(Res.string.checking_release),\n                    )\n                }\n\n                val latestRelease =\n                    try {\n                        appsRepository.getLatestRelease(owner, repo)\n                    } catch (e: RateLimitException) {\n                        throw e\n                    } catch (e: Exception) {\n                        logger.debug(\"Could not fetch release for validation: ${e.message}\")\n                        return@launch\n                    }\n\n                if (latestRelease == null) {\n                    appsRepository.linkAppToRepo(selectedApp.toDomain(), repoInfo)\n                    _state.update {\n                        it.copy(\n                            isValidatingRepo = false,\n                            linkValidationStatus = null,\n                            showLinkSheet = false,\n                        )\n                    }\n                    _events.send(AppsEvent.AppLinkedSuccessfully(selectedApp.appName))\n                    _events.send(\n                        AppsEvent.ShowSuccess(\n                            getString(\n                                Res.string.app_linked_success,\n                                selectedApp.appName,\n                                repoInfo.owner,\n                                repoInfo.name,\n                            ),\n                        ),\n                    )\n                    return@launch\n                }\n\n                val installableAssets =\n                    latestRelease\n                        .assets\n                        .filter { installer.isAssetInstallable(it.name) }\n                        .map { it.toUi() }\n                        .toImmutableList()\n                if (installableAssets.isEmpty()) {\n                    appsRepository.linkAppToRepo(selectedApp.toDomain(), repoInfo)\n                    _state.update {\n                        it.copy(\n                            isValidatingRepo = false,\n                            linkValidationStatus = null,\n                            showLinkSheet = false,\n                        )\n                    }\n                    _events.send(AppsEvent.AppLinkedSuccessfully(selectedApp.appName))\n                    _events.send(\n                        AppsEvent.ShowSuccess(\n                            getString(\n                                Res.string.app_linked_success,\n                                selectedApp.appName,\n                                repoInfo.owner,\n                                repoInfo.name,\n                            ),\n                        ),\n                    )\n                    return@launch\n                }\n\n                _state.update {\n                    it.copy(\n                        isValidatingRepo = false,\n                        linkValidationStatus = null,\n                        linkStep = LinkStep.PickAsset,\n                        linkInstallableAssets = installableAssets,\n                    )\n                }\n            } catch (_: RateLimitException) {\n                _state.update {\n                    it.copy(\n                        isValidatingRepo = false,\n                        linkValidationStatus = null,\n                        repoValidationError = getString(Res.string.rate_limit_try_again),\n                    )\n                }\n            } catch (e: Exception) {\n                logger.error(\"Failed to link app: ${e.message}\")\n                _state.update {\n                    it.copy(\n                        isValidatingRepo = false,\n                        linkValidationStatus = null,\n                        repoValidationError = getString(Res.string.failed_to_link, e.message ?: \"\"),\n                    )\n                }\n            }\n        }\n    }\n\n    private fun validateWithAsset(asset: GithubAssetUi) {\n        val selectedApp = _state.value.selectedDeviceApp ?: return\n        val repoInfo = _state.value.fetchedRepoInfo ?: return\n\n        viewModelScope.launch {\n            _state.update {\n                it.copy(\n                    linkSelectedAsset = asset,\n                    linkDownloadProgress = 0,\n                    linkValidationStatus = getString(Res.string.downloading_for_verification),\n                    repoValidationError = null,\n                )\n            }\n\n            var filePath: String? = null\n            try {\n                downloader.download(asset.downloadUrl, asset.name).collect { progress ->\n                    _state.update { it.copy(linkDownloadProgress = progress.percent) }\n                }\n\n                filePath = downloader.getDownloadedFilePath(asset.name)\n                if (filePath == null) {\n                    _state.update {\n                        it.copy(\n                            linkDownloadProgress = null,\n                            linkValidationStatus = null,\n                            repoValidationError = getString(Res.string.download_failed),\n                        )\n                    }\n                    return@launch\n                }\n\n                _state.update {\n                    it.copy(\n                        linkDownloadProgress = 100,\n                        linkValidationStatus = getString(Res.string.verifying_signing_key),\n                    )\n                }\n\n                val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(filePath)\n                if (apkInfo == null) {\n                    logger.debug(\"Could not extract APK info for validation, linking anyway\")\n                    appsRepository.linkAppToRepo(selectedApp.toDomain(), repoInfo.toDomain())\n                    _state.update {\n                        it.copy(\n                            linkDownloadProgress = null,\n                            linkValidationStatus = null,\n                            showLinkSheet = false,\n                        )\n                    }\n                    _events.send(AppsEvent.AppLinkedSuccessfully(selectedApp.appName))\n                    _events.send(\n                        AppsEvent.ShowSuccess(\n                            getString(\n                                Res.string.app_linked_success,\n                                selectedApp.appName,\n                                repoInfo.owner,\n                                repoInfo.name,\n                            ),\n                        ),\n                    )\n                    return@launch\n                }\n\n                if (apkInfo.packageName != selectedApp.packageName) {\n                    _state.update {\n                        it.copy(\n                            linkDownloadProgress = null,\n                            linkValidationStatus = null,\n                            repoValidationError =\n                                getString(\n                                    Res.string.package_name_mismatch,\n                                    apkInfo.packageName,\n                                    selectedApp.packageName,\n                                ),\n                        )\n                    }\n                    return@launch\n                }\n\n                val deviceFingerprint = selectedApp.signingFingerprint\n                val apkFingerprint = apkInfo.signingFingerprint\n\n                if (deviceFingerprint != null && apkFingerprint != null && deviceFingerprint != apkFingerprint) {\n                    _state.update {\n                        it.copy(\n                            linkDownloadProgress = null,\n                            linkValidationStatus = null,\n                            repoValidationError = getString(Res.string.signing_key_mismatch_link),\n                        )\n                    }\n                    return@launch\n                }\n\n                appsRepository.linkAppToRepo(selectedApp.toDomain(), repoInfo.toDomain())\n                _state.update {\n                    it.copy(\n                        linkDownloadProgress = null,\n                        linkValidationStatus = null,\n                        showLinkSheet = false,\n                    )\n                }\n                _events.send(AppsEvent.AppLinkedSuccessfully(selectedApp.appName))\n                _events.send(\n                    AppsEvent.ShowSuccess(\n                        getString(\n                            Res.string.app_linked_success,\n                            selectedApp.appName,\n                            repoInfo.owner,\n                            repoInfo.name,\n                        ),\n                    ),\n                )\n            } catch (_: RateLimitException) {\n                _state.update {\n                    it.copy(\n                        linkDownloadProgress = null,\n                        linkValidationStatus = null,\n                        repoValidationError = getString(Res.string.rate_limit_try_again),\n                    )\n                }\n            } catch (e: Exception) {\n                logger.error(\"Failed to validate and link app: ${e.message}\")\n                _state.update {\n                    it.copy(\n                        linkDownloadProgress = null,\n                        linkValidationStatus = null,\n                        repoValidationError = getString(Res.string.failed_to_link, e.message ?: \"\"),\n                    )\n                }\n            } finally {\n                try {\n                    if (filePath != null) File(filePath).delete()\n                } catch (_: Exception) {\n                }\n            }\n        }\n    }\n\n    private fun parseGithubUrl(input: String): Pair<String, String>? {\n        val normalized =\n            input\n                .trim()\n                .removePrefix(\"https://\")\n                .removePrefix(\"http://\")\n                .removePrefix(\"www.\")\n                .substringBefore(\"?\")\n                .substringBefore(\"#\")\n                .removeSuffix(\"/\")\n\n        val parts = normalized.split(\"/\")\n        if (parts.size < 3) return null\n        if (!parts[0].equals(\"github.com\", ignoreCase = true)) return null\n\n        val owner = parts[1]\n        val repo = parts[2]\n\n        if (owner.isBlank() || repo.isBlank()) return null\n        if (owner.length > 39 || repo.length > 100) return null\n\n        return owner to repo\n    }\n\n    private fun exportApps() {\n        viewModelScope.launch {\n            _state.update { it.copy(isExporting = true) }\n            try {\n                val json = appsRepository.exportApps()\n                val fileName = \"github-store-apps-${System.currentTimeMillis()}.json\"\n                shareManager.shareFile(fileName, json)\n            } catch (e: Exception) {\n                logger.error(\"Export failed: ${e.message}\")\n                _events.send(\n                    AppsEvent.ShowError(\n                        getString(\n                            Res.string.export_failed,\n                            e.message ?: \"\",\n                        ),\n                    ),\n                )\n            } finally {\n                _state.update { it.copy(isExporting = false) }\n            }\n        }\n    }\n\n    private fun importAppsFromFile() {\n        shareManager.pickFile(\"application/json\") { content ->\n            if (content != null) {\n                viewModelScope.launch {\n                    importApps(content)\n                }\n            }\n        }\n    }\n\n    private suspend fun importApps(json: String) {\n        _state.update { it.copy(isImporting = true) }\n        try {\n            val result = appsRepository.importApps(json)\n            _events.send(AppsEvent.ImportComplete(result))\n            _events.send(\n                AppsEvent.ShowSuccess(\n                    getString(Res.string.imported_apps_summary, result.imported) +\n                        (\n                            if (result.skipped > 0) {\n                                getString(\n                                    Res.string.imported_skipped,\n                                    result.skipped,\n                                )\n                            } else {\n                                \"\"\n                            }\n                        ) +\n                        (\n                            if (result.failed > 0) {\n                                getString(\n                                    Res.string.imported_failed,\n                                    result.failed,\n                                )\n                            } else {\n                                \"\"\n                            }\n                        ),\n                ),\n            )\n        } catch (e: Exception) {\n            logger.error(\"Import failed: ${e.message}\")\n            _events.send(AppsEvent.ShowError(getString(Res.string.import_failed, e.message ?: \"\")))\n        } finally {\n            _state.update { it.copy(isImporting = false) }\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n\n        updateAllJob?.cancel()\n        activeUpdates.values.forEach { it.cancel() }\n\n        viewModelScope.launch {\n            _state.value.apps.forEach { appItem ->\n                if (appItem.updateState != UpdateState.Idle &&\n                    appItem.updateState != UpdateState.Success\n                ) {\n                    appItem.installedApp.latestAssetName?.let { assetName ->\n                        downloader.cancelDownload(assetName)\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt",
    "content": "package zed.rainxch.apps.presentation.components\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.apps.presentation.AppsAction\nimport zed.rainxch.apps.presentation.AppsState\nimport zed.rainxch.apps.presentation.LinkStep\nimport zed.rainxch.apps.presentation.model.DeviceAppUi\nimport zed.rainxch.apps.presentation.model.GithubAssetUi\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LinkAppBottomSheet(\n    state: AppsState,\n    onAction: (AppsAction) -> Unit,\n) {\n    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)\n\n    ModalBottomSheet(\n        onDismissRequest = { onAction(AppsAction.OnDismissLinkSheet) },\n        sheetState = sheetState,\n    ) {\n        AnimatedContent(\n            targetState = state.linkStep,\n            transitionSpec = {\n                if (targetState.ordinal > initialState.ordinal) {\n                    (slideInHorizontally { it } + fadeIn()) togetherWith\n                        (slideOutHorizontally { -it } + fadeOut())\n                } else {\n                    (slideInHorizontally { -it } + fadeIn()) togetherWith\n                        (slideOutHorizontally { it } + fadeOut())\n                }\n            },\n            label = \"link_step\",\n        ) { step ->\n            when (step) {\n                LinkStep.PickApp -> PickAppStep(\n                    deviceApps = state.filteredDeviceApps,\n                    searchQuery = state.deviceAppSearchQuery,\n                    onSearchChange = { onAction(AppsAction.OnDeviceAppSearchChange(it)) },\n                    onAppSelected = { onAction(AppsAction.OnDeviceAppSelected(it)) },\n                )\n\n                LinkStep.EnterUrl -> EnterUrlStep(\n                    selectedApp = state.selectedDeviceApp,\n                    repoUrl = state.repoUrl,\n                    isValidating = state.isValidatingRepo,\n                    validationError = state.repoValidationError,\n                    validationStatus = state.linkValidationStatus,\n                    onUrlChanged = { onAction(AppsAction.OnRepoUrlChanged(it)) },\n                    onConfirm = { onAction(AppsAction.OnValidateAndLinkRepo) },\n                    onBack = { onAction(AppsAction.OnBackToAppPicker) },\n                )\n\n                LinkStep.PickAsset -> PickAssetStep(\n                    assets = state.linkInstallableAssets,\n                    selectedAsset = state.linkSelectedAsset,\n                    downloadProgress = state.linkDownloadProgress,\n                    validationStatus = state.linkValidationStatus,\n                    validationError = state.repoValidationError,\n                    onAssetSelected = { onAction(AppsAction.OnLinkAssetSelected(it)) },\n                    onBack = { onAction(AppsAction.OnBackToEnterUrl) },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PickAppStep(\n    deviceApps: List<DeviceAppUi>,\n    searchQuery: String,\n    onSearchChange: (String) -> Unit,\n    onAppSelected: (DeviceAppUi) -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp),\n    ) {\n        Text(\n            text = stringResource(Res.string.link_app_title),\n            style = MaterialTheme.typography.titleLarge,\n            fontWeight = FontWeight.Bold,\n        )\n\n        Spacer(Modifier.height(4.dp))\n\n        Text(\n            text = stringResource(Res.string.pick_installed_app),\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n\n        Spacer(Modifier.height(12.dp))\n\n        TextField(\n            value = searchQuery,\n            onValueChange = onSearchChange,\n            modifier = Modifier\n                .fillMaxWidth()\n                .clip(RoundedCornerShape(16.dp)),\n            placeholder = {\n                Text(stringResource(Res.string.search_apps_hint))\n            },\n            leadingIcon = {\n                Icon(\n                    imageVector = Icons.Default.Search,\n                    contentDescription = null,\n                )\n            },\n            singleLine = true,\n            colors = TextFieldDefaults.colors(\n                focusedIndicatorColor = Color.Transparent,\n                unfocusedIndicatorColor = Color.Transparent,\n            ),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(400.dp),\n        ) {\n            items(\n                items = deviceApps,\n                key = { it.packageName },\n            ) { app ->\n                DeviceAppItem(\n                    app = app,\n                    onClick = { onAppSelected(app) },\n                )\n                HorizontalDivider(\n                    color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f),\n                )\n            }\n\n            if (deviceApps.isEmpty()) {\n                item {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(32.dp),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Text(\n                            text = stringResource(Res.string.no_apps_found),\n                            style = MaterialTheme.typography.bodyLarge,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n        }\n\n        Spacer(Modifier.height(16.dp))\n    }\n}\n\n@Composable\nprivate fun DeviceAppItem(\n    app: DeviceAppUi,\n    onClick: () -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable(onClick = onClick)\n            .padding(vertical = 12.dp, horizontal = 4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(\n            modifier = Modifier.weight(1f),\n        ) {\n            Text(\n                text = app.appName,\n                style = MaterialTheme.typography.bodyLarge,\n                fontWeight = FontWeight.Medium,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Text(\n                text = app.packageName,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n\n        Spacer(Modifier.width(8.dp))\n\n        app.versionName?.let { version ->\n            Text(\n                text = version,\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.outline,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun EnterUrlStep(\n    selectedApp: DeviceAppUi?,\n    repoUrl: String,\n    isValidating: Boolean,\n    validationError: String?,\n    validationStatus: String?,\n    onUrlChanged: (String) -> Unit,\n    onConfirm: () -> Unit,\n    onBack: () -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp)\n            .padding(bottom = 24.dp),\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            IconButton(onClick = onBack) {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                    contentDescription = null,\n                )\n            }\n\n            Text(\n                text = stringResource(Res.string.link_app_title),\n                style = MaterialTheme.typography.titleLarge,\n                fontWeight = FontWeight.Bold,\n            )\n        }\n\n        Spacer(Modifier.height(16.dp))\n\n        // Selected app info\n        if (selectedApp != null) {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clip(RoundedCornerShape(12.dp))\n                    .padding(12.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = selectedApp.appName,\n                        style = MaterialTheme.typography.titleMedium,\n                        fontWeight = FontWeight.SemiBold,\n                    )\n                    Text(\n                        text = selectedApp.packageName,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n                selectedApp.versionName?.let {\n                    Text(\n                        text = it,\n                        style = MaterialTheme.typography.labelLarge,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            }\n        }\n\n        Spacer(Modifier.height(16.dp))\n\n        OutlinedTextField(\n            value = repoUrl,\n            onValueChange = onUrlChanged,\n            modifier = Modifier.fillMaxWidth(),\n            label = { Text(stringResource(Res.string.enter_repo_url)) },\n            placeholder = { Text(stringResource(Res.string.repo_url_hint)) },\n            singleLine = true,\n            isError = validationError != null,\n            supportingText = validationError?.let {\n                { Text(it, color = MaterialTheme.colorScheme.error) }\n            },\n            shape = RoundedCornerShape(12.dp),\n        )\n\n        Spacer(Modifier.height(20.dp))\n\n        FilledTonalButton(\n            onClick = onConfirm,\n            modifier = Modifier.fillMaxWidth(),\n            enabled = repoUrl.isNotBlank() && !isValidating,\n            shape = RoundedCornerShape(12.dp),\n        ) {\n            if (isValidating) {\n                CircularProgressIndicator(\n                    modifier = Modifier.size(20.dp),\n                    strokeWidth = 2.dp,\n                )\n                Spacer(Modifier.width(8.dp))\n                Text(stringResource(Res.string.validating_repo))\n            } else {\n                Text(\n                    text = stringResource(Res.string.link_and_track),\n                    style = MaterialTheme.typography.titleSmall,\n                    fontWeight = FontWeight.Bold,\n                )\n            }\n        }\n\n        if (isValidating && validationStatus != null) {\n            Spacer(Modifier.height(8.dp))\n            Text(\n                text = validationStatus,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                modifier = Modifier.fillMaxWidth(),\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PickAssetStep(\n    assets: List<GithubAssetUi>,\n    selectedAsset: GithubAssetUi?,\n    downloadProgress: Int?,\n    validationStatus: String?,\n    validationError: String?,\n    onAssetSelected: (GithubAssetUi) -> Unit,\n    onBack: () -> Unit,\n) {\n    val isProcessing = selectedAsset != null\n\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp)\n            .padding(bottom = 24.dp),\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            IconButton(onClick = onBack, enabled = !isProcessing) {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                    contentDescription = null,\n                )\n            }\n\n            Text(\n                text = stringResource(Res.string.select_asset_title),\n                style = MaterialTheme.typography.titleLarge,\n                fontWeight = FontWeight.Bold,\n            )\n        }\n\n        Spacer(Modifier.height(4.dp))\n\n        Text(\n            text = stringResource(Res.string.select_asset_description),\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            modifier = Modifier.padding(horizontal = 4.dp),\n        )\n\n        Spacer(Modifier.height(12.dp))\n\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(300.dp),\n        ) {\n            items(\n                items = assets,\n                key = { it.id },\n            ) { asset ->\n                val isSelected = selectedAsset?.id == asset.id\n\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .then(\n                            if (isSelected) {\n                                Modifier.background(\n                                    MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f),\n                                    RoundedCornerShape(8.dp),\n                                )\n                            } else {\n                                Modifier\n                            },\n                        )\n                        .clickable(enabled = !isProcessing) { onAssetSelected(asset) }\n                        .padding(vertical = 12.dp, horizontal = 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = asset.name,\n                            style = MaterialTheme.typography.bodyMedium,\n                            fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                            maxLines = 2,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                        Text(\n                            text = formatFileSize(asset.size),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n\n                    if (isSelected && downloadProgress != null) {\n                        Spacer(Modifier.width(8.dp))\n                        CircularProgressIndicator(\n                            progress = { downloadProgress / 100f },\n                            modifier = Modifier.size(24.dp),\n                            strokeWidth = 2.dp,\n                        )\n                        Spacer(Modifier.width(4.dp))\n                        Text(\n                            text = \"$downloadProgress%\",\n                            style = MaterialTheme.typography.labelSmall,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                    }\n                }\n\n                HorizontalDivider(\n                    color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f),\n                )\n            }\n        }\n\n        if (validationStatus != null) {\n            Spacer(Modifier.height(8.dp))\n            Text(\n                text = validationStatus,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n\n        if (validationError != null) {\n            Spacer(Modifier.height(8.dp))\n            Text(\n                text = validationError,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.error,\n            )\n        }\n    }\n}\n\nprivate fun formatFileSize(bytes: Long): String =\n    when {\n        bytes >= 1_073_741_824 -> \"%.1f GB\".format(bytes / 1_073_741_824.0)\n        bytes >= 1_048_576 -> \"%.1f MB\".format(bytes / 1_048_576.0)\n        bytes >= 1_024 -> \"%.1f KB\".format(bytes / 1_024.0)\n        else -> \"$bytes B\"\n    }\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/DeviceAppMapper.kt",
    "content": "package zed.rainxch.apps.presentation.mappers\n\nimport zed.rainxch.apps.presentation.model.DeviceAppUi\nimport zed.rainxch.core.domain.model.DeviceApp\n\nfun DeviceApp.toUi() : DeviceAppUi {\n    return DeviceAppUi(\n        packageName = packageName,\n        appName = appName,\n        versionName = versionName,\n        versionCode = versionCode,\n        signingFingerprint = signingFingerprint\n    )\n}\n\nfun DeviceAppUi.toDomain() : DeviceApp {\n    return DeviceApp(\n        packageName = packageName,\n        appName = appName,\n        versionName = versionName,\n        versionCode = versionCode,\n        signingFingerprint = signingFingerprint\n    )\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/GithubAssetMapper.kt",
    "content": "package zed.rainxch.apps.presentation.mappers\n\nimport zed.rainxch.apps.presentation.model.GithubAssetUi\nimport zed.rainxch.apps.presentation.model.GithubUserUi\nimport zed.rainxch.core.domain.model.GithubAsset\n\nfun GithubAsset.toUi(): GithubAssetUi {\n    return GithubAssetUi(\n        id = id,\n        name = name,\n        contentType = contentType,\n        size = size,\n        downloadUrl = downloadUrl,\n        uploader = GithubUserUi(\n            id = uploader.id,\n            login = uploader.login,\n            avatarUrl = uploader.avatarUrl,\n            htmlUrl = uploader.htmlUrl,\n        ),\n    )\n}\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/GithubRepoInfoMapper.kt",
    "content": "package zed.rainxch.apps.presentation.mappers\n\nimport zed.rainxch.apps.domain.model.GithubRepoInfo\nimport zed.rainxch.apps.presentation.model.GithubRepoInfoUi\n\nfun GithubRepoInfo.toUi(): GithubRepoInfoUi =\n    GithubRepoInfoUi(\n        id = id,\n        name = name,\n        owner = owner,\n        ownerAvatarUrl = ownerAvatarUrl,\n        description = description,\n        language = language,\n        htmlUrl = htmlUrl,\n        latestReleaseTag = latestReleaseTag,\n    )\n\nfun GithubRepoInfoUi.toDomain(): GithubRepoInfo =\n    GithubRepoInfo(\n        id = id,\n        name = name,\n        owner = owner,\n        ownerAvatarUrl = ownerAvatarUrl,\n        description = description,\n        language = language,\n        htmlUrl = htmlUrl,\n        latestReleaseTag = latestReleaseTag,\n    )\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/InstalledAppMapper.kt",
    "content": "package zed.rainxch.apps.presentation.mappers\n\nimport zed.rainxch.apps.presentation.model.InstalledAppUi\nimport zed.rainxch.core.domain.model.InstalledApp\n\nfun InstalledApp.toUi(): InstalledAppUi =\n    InstalledAppUi(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        installedVersion = installedVersion,\n        installedAssetName = installedAssetName,\n        installedAssetUrl = installedAssetUrl,\n        latestVersion = latestVersion,\n        latestAssetName = latestAssetName,\n        latestAssetUrl = latestAssetUrl,\n        latestAssetSize = latestAssetSize,\n        installSource = installSource,\n        installedAt = installedAt,\n        lastCheckedAt = lastCheckedAt,\n        lastUpdatedAt = lastUpdatedAt,\n        isUpdateAvailable = isUpdateAvailable,\n        updateCheckEnabled = updateCheckEnabled,\n        releaseNotes = releaseNotes,\n        systemArchitecture = systemArchitecture,\n        fileExtension = fileExtension,\n        isPendingInstall = isPendingInstall,\n        installedVersionName = installedVersionName,\n        installedVersionCode = installedVersionCode,\n        latestVersionName = latestVersionName,\n        latestVersionCode = latestVersionCode,\n        packageName = packageName,\n        appName = appName,\n        signingFingerprint = signingFingerprint,\n    )\n\nfun InstalledAppUi.toDomain(): InstalledApp =\n    InstalledApp(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        installedVersion = installedVersion,\n        installedAssetName = installedAssetName,\n        installedAssetUrl = installedAssetUrl,\n        latestVersion = latestVersion,\n        latestAssetName = latestAssetName,\n        latestAssetUrl = latestAssetUrl,\n        latestAssetSize = latestAssetSize,\n        installSource = installSource,\n        installedAt = installedAt,\n        lastCheckedAt = lastCheckedAt,\n        lastUpdatedAt = lastUpdatedAt,\n        isUpdateAvailable = isUpdateAvailable,\n        updateCheckEnabled = updateCheckEnabled,\n        releaseNotes = releaseNotes,\n        systemArchitecture = systemArchitecture,\n        fileExtension = fileExtension,\n        isPendingInstall = isPendingInstall,\n        installedVersionName = installedVersionName,\n        installedVersionCode = installedVersionCode,\n        latestVersionName = latestVersionName,\n        latestVersionCode = latestVersionCode,\n        packageName = packageName,\n        appName = appName,\n        signingFingerprint = signingFingerprint,\n    )\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/AppItem.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\ndata class AppItem(\n    val installedApp: InstalledAppUi,\n    val updateState: UpdateState = UpdateState.Idle,\n    val downloadProgress: Int? = null,\n    val error: String? = null,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/DeviceAppUi.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\ndata class DeviceAppUi(\n    val packageName: String,\n    val appName: String,\n    val versionName: String?,\n    val versionCode: Long,\n    val signingFingerprint: String?,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/GithubAssetUi.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\ndata class GithubAssetUi(\n    val id: Long,\n    val name: String,\n    val contentType: String,\n    val size: Long,\n    val downloadUrl: String,\n    val uploader: GithubUserUi,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/GithubRepoInfoUi.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\ndata class GithubRepoInfoUi(\n    val id: Long,\n    val name: String,\n    val owner: String,\n    val ownerAvatarUrl: String,\n    val description: String?,\n    val language: String?,\n    val htmlUrl: String,\n    val latestReleaseTag: String?,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/GithubUserUi.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\nimport kotlinx.serialization.Serializable\n\ndata class GithubUserUi(\n    val id: Long,\n    val login: String,\n    val avatarUrl: String,\n    val htmlUrl: String,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/InstalledAppUi.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\nimport zed.rainxch.core.domain.model.InstallSource\n\ndata class InstalledAppUi(\n    val packageName: String,\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val installedVersion: String,\n    val installedAssetName: String?,\n    val installedAssetUrl: String?,\n    val latestVersion: String?,\n    val latestAssetName: String?,\n    val latestAssetUrl: String?,\n    val latestAssetSize: Long?,\n    val appName: String,\n    val installSource: InstallSource,\n    val installedAt: Long,\n    val lastCheckedAt: Long,\n    val lastUpdatedAt: Long,\n    val isUpdateAvailable: Boolean,\n    val signingFingerprint: String?,\n    val updateCheckEnabled: Boolean = true,\n    val releaseNotes: String? = \"\",\n    val systemArchitecture: String,\n    val fileExtension: String,\n    val isPendingInstall: Boolean = false,\n    val installedVersionName: String? = null,\n    val installedVersionCode: Long = 0L,\n    val latestVersionName: String? = null,\n    val latestVersionCode: Long? = null,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/UpdateAllProgress.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\ndata class UpdateAllProgress(\n    val current: Int,\n    val total: Int,\n    val currentAppName: String,\n)\n"
  },
  {
    "path": "feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/UpdateState.kt",
    "content": "package zed.rainxch.apps.presentation.model\n\nsealed class UpdateState {\n    data object Idle : UpdateState()\n\n    data object CheckingUpdate : UpdateState()\n\n    data object Downloading : UpdateState()\n\n    data object Installing : UpdateState()\n\n    data object Success : UpdateState()\n\n    data class Error(\n        val message: String,\n    ) : UpdateState()\n}\n"
  },
  {
    "path": "feature/auth/CLAUDE.md",
    "content": "# CLAUDE.md - Auth Feature\n\n## Purpose\n\nGitHub OAuth authentication using the **device flow**. Users authenticate by visiting a URL and entering a code displayed in the app. No browser redirect needed, making it suitable for both Android and Desktop.\n\n## Module Structure\n\n```\nfeature/auth/\n├── domain/\n│   └── repository/AuthenticationRepository.kt  # Device flow interface\n├── data/\n│   ├── di/SharedModule.kt            # Koin: authModule\n│   ├── repository/AuthenticationRepositoryImpl.kt  # OAuth device flow implementation\n│   └── network/GitHubAuthApi.kt      # GitHub OAuth API endpoints\n└── presentation/\n    ├── AuthenticationViewModel.kt     # Manages device flow lifecycle\n    ├── AuthenticationState.kt         # Code, URL, loading, error\n    ├── AuthenticationAction.kt        # StartAuth, Cancel, etc.\n    ├── AuthenticationEvent.kt         # One-off events\n    ├── AuthenticationRoot.kt          # UI: displays code + verification URL\n    └── components/                    # Auth UI components\n```\n\n## Key Interfaces\n\n```kotlin\ninterface AuthenticationRepository {\n    val accessTokenFlow: Flow<String?>\n    suspend fun startDeviceFlow(): GithubDeviceStart\n    suspend fun awaitDeviceToken(start: GithubDeviceStart): GithubDeviceTokenSuccess\n}\n```\n\n## Navigation\n\nRoute: `GithubStoreGraph.AuthenticationScreen` (data object, no params)\n\n## Implementation Notes\n\n- Uses GitHub's [device authorization flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)\n- `startDeviceFlow()` returns a user code + verification URL to display\n- `awaitDeviceToken()` polls GitHub until the user completes verification\n- Token is stored via `TokenStore` in core/data (DataStore-backed)\n- `GITHUB_CLIENT_ID` must be set in `local.properties` for builds\n- `accessTokenFlow` is observed app-wide by `MainViewModel` for auth state\n"
  },
  {
    "path": "feature/auth/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/auth/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.auth.domain)\n\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/auth/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/di/SharedModule.kt",
    "content": "package zed.rainxch.auth.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.auth.data.repository.AuthenticationRepositoryImpl\nimport zed.rainxch.auth.domain.repository.AuthenticationRepository\n\nval authModule =\n    module {\n        single<AuthenticationRepository> {\n            AuthenticationRepositoryImpl(\n                tokenStore = get(),\n                logger = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/network/GitHubAuthApi.kt",
    "content": "package zed.rainxch.auth.data.network\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.plugins.HttpRequestRetry\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.timeout\nimport io.ktor.client.request.accept\nimport io.ktor.client.request.forms.FormDataContent\nimport io.ktor.client.request.post\nimport io.ktor.client.request.setBody\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.ContentType\nimport io.ktor.http.HttpHeaders\nimport io.ktor.http.HttpStatusCode\nimport io.ktor.http.Parameters\nimport io.ktor.http.contentType\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.coroutines.delay\nimport kotlinx.serialization.json.Json\nimport zed.rainxch.core.data.dto.GithubDeviceStartDto\nimport zed.rainxch.core.data.dto.GithubDeviceTokenErrorDto\nimport zed.rainxch.core.data.dto.GithubDeviceTokenSuccessDto\n\nobject GitHubAuthApi {\n    private val json =\n        Json {\n            ignoreUnknownKeys = true\n            isLenient = true\n        }\n\n    val http by lazy {\n        HttpClient {\n            install(ContentNegotiation) { json(json) }\n\n            install(HttpTimeout) {\n                requestTimeoutMillis = 60_000\n                connectTimeoutMillis = 30_000\n                socketTimeoutMillis = 30_000\n            }\n\n            install(HttpRequestRetry) {\n                retryOnServerErrors(maxRetries = 2)\n                retryOnException(maxRetries = 2, retryOnTimeout = true)\n                exponentialDelay()\n            }\n        }\n    }\n\n    suspend fun startDeviceFlow(clientId: String): GithubDeviceStartDto =\n        withRetry(maxAttempts = 3, initialDelay = 1000) {\n            val res =\n                http.post(\"https://github.com/login/device/code\") {\n                    accept(ContentType.Application.Json)\n                    headers.append(HttpHeaders.UserAgent, \"GithubStore/1.0 (DeviceFlow)\")\n                    contentType(ContentType.Application.FormUrlEncoded)\n                    setBody(\n                        FormDataContent(\n                            Parameters.build {\n                                append(\"client_id\", clientId)\n                            },\n                        ),\n                    )\n                }\n            val status = res.status\n            val text = res.bodyAsText()\n\n            if (status !in HttpStatusCode.OK..HttpStatusCode.MultipleChoices) {\n                error(\n                    buildString {\n                        append(\"GitHub device/code HTTP \")\n                        append(status.value)\n                        append(\" \")\n                        append(status.description)\n                        append(\". Body: \")\n                        append(text.take(300))\n                    },\n                )\n            }\n\n            try {\n                json.decodeFromString(GithubDeviceStartDto.serializer(), text)\n            } catch (_: Throwable) {\n                try {\n                    val err = json.decodeFromString(GithubDeviceTokenErrorDto.serializer(), text)\n                    error(\"${err.error}: ${err.errorDescription ?: \"\"}\".trim())\n                } catch (_: Throwable) {\n                    error(\"Unexpected response from GitHub: $text\")\n                }\n            }\n        }\n\n    suspend fun pollDeviceToken(\n        clientId: String,\n        deviceCode: String,\n    ): Result<GithubDeviceTokenSuccessDto> {\n        return try {\n            val res =\n                http.post(\"https://github.com/login/oauth/access_token\") {\n                    accept(ContentType.Application.Json)\n                    headers.append(HttpHeaders.UserAgent, \"GithubStore/1.0 (DeviceFlow)\")\n                    contentType(ContentType.Application.FormUrlEncoded)\n\n                    timeout {\n                        socketTimeoutMillis = 30_000\n                    }\n\n                    setBody(\n                        FormDataContent(\n                            Parameters.build {\n                                append(\"client_id\", clientId)\n                                append(\"device_code\", deviceCode)\n                                append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:device_code\")\n                            },\n                        ),\n                    )\n                }\n            val status = res.status\n            val text = res.body<String>()\n\n            if (status !in HttpStatusCode.OK..HttpStatusCode.MultipleChoices) {\n                return Result.failure(\n                    IllegalStateException(\n                        \"GitHub access_token HTTP ${status.value} ${status.description}\",\n                    ),\n                )\n            }\n\n            try {\n                val ok = json.decodeFromString(GithubDeviceTokenSuccessDto.serializer(), text)\n                Result.success(ok)\n            } catch (_: Throwable) {\n                val err = json.decodeFromString(GithubDeviceTokenErrorDto.serializer(), text)\n                val message =\n                    buildString {\n                        append(err.error)\n                        val desc = err.errorDescription\n                        if (!desc.isNullOrBlank()) {\n                            append(\": \")\n                            append(desc)\n                        }\n                    }\n                Result.failure(IllegalStateException(message))\n            }\n        } catch (e: Exception) {\n            Result.failure(e)\n        }\n    }\n\n    private suspend fun <T> withRetry(\n        maxAttempts: Int = 3,\n        initialDelay: Long = 1000,\n        maxDelay: Long = 5000,\n        factor: Double = 2.0,\n        block: suspend () -> T,\n    ): T {\n        var currentDelay = initialDelay\n        repeat(maxAttempts - 1) { attempt ->\n            try {\n                return block()\n            } catch (e: Exception) {\n                println(\"⚠️ Attempt ${attempt + 1} failed: ${e.message}\")\n                if (attempt == maxAttempts - 2) throw e\n            }\n            delay(currentDelay)\n            currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)\n        }\n        return block()\n    }\n}\n"
  },
  {
    "path": "feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/repository/AuthenticationRepositoryImpl.kt",
    "content": "package zed.rainxch.auth.data.repository\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.withContext\nimport zed.rainxch.auth.data.network.GitHubAuthApi\nimport zed.rainxch.auth.domain.repository.AuthenticationRepository\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.data.mappers.toData\nimport zed.rainxch.core.data.mappers.toDomain\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.GithubDeviceStart\nimport zed.rainxch.core.domain.model.GithubDeviceTokenSuccess\nimport zed.rainxch.feature.auth.data.BuildKonfig\nimport java.util.concurrent.TimeoutException\n\nclass AuthenticationRepositoryImpl(\n    private val tokenStore: TokenStore,\n    private val logger: GitHubStoreLogger,\n) : AuthenticationRepository {\n    override val accessTokenFlow: Flow<String?>\n        get() = tokenStore.tokenFlow().map { it?.accessToken }\n\n    override suspend fun startDeviceFlow(): GithubDeviceStart =\n        withContext(Dispatchers.IO) {\n            val clientId = BuildKonfig.GITHUB_CLIENT_ID\n            require(clientId.isNotBlank()) {\n                \"Missing GitHub CLIENT_ID. Add GITHUB_CLIENT_ID to local.properties.\"\n            }\n\n            try {\n                val result = GitHubAuthApi.startDeviceFlow(clientId)\n                logger.debug(\"✅ Device flow started. User code: ${result.userCode}\")\n                result.toDomain()\n            } catch (e: Exception) {\n                logger.debug(\"❌ Failed to start device flow: ${e.message}\")\n                throw Exception(\n                    \"Failed to start GitHub authentication. \" +\n                        \"Please check your internet connection and try again.\",\n                    e,\n                )\n            }\n        }\n\n    override suspend fun awaitDeviceToken(start: GithubDeviceStart): GithubDeviceTokenSuccess =\n        withContext(Dispatchers.IO) {\n            val clientId = BuildKonfig.GITHUB_CLIENT_ID\n            val timeoutMs = start.expiresInSec * 1000L\n            val startTime = System.currentTimeMillis()\n\n            val initialJitter = (0..2000).random().toLong()\n            delay(initialJitter)\n\n            var pollingInterval = (start.intervalSec.coerceAtLeast(5)) * 1000L\n            var consecutiveNetworkErrors = 0\n            var consecutiveUnknownErrors = 0\n            var slowDownCount = 0\n\n            logger.debug(\"⏱️ Polling started. Timeout: ${start.expiresInSec}s, Interval: ${start.intervalSec}s\")\n\n            while (isActive) {\n                if (System.currentTimeMillis() - startTime >= timeoutMs) {\n                    throw TimeoutException(\n                        \"Authentication timed out after ${start.expiresInSec} seconds. Please try again.\",\n                    )\n                }\n\n                try {\n                    val res = GitHubAuthApi.pollDeviceToken(clientId, start.deviceCode)\n                    val success = res.getOrNull()?.toDomain()\n\n                    if (success != null) {\n                        logger.debug(\"✅ Token received! Saving...\")\n\n                        saveTokenWithVerification(success)\n\n                        logger.debug(\"✅ Token saved and verified successfully!\")\n                        return@withContext success\n                    }\n\n                    val error = res.exceptionOrNull()\n                    val errorMsg = (error?.message ?: \"\").lowercase()\n\n                    when {\n                        \"authorization_pending\" in errorMsg -> {\n                            consecutiveNetworkErrors = 0\n                            consecutiveUnknownErrors = 0\n                            if (slowDownCount > 0) slowDownCount--\n\n                            logger.debug(\"📡 Waiting for user authorization...\")\n                            delay(pollingInterval + (0..1000).random())\n                        }\n\n                        \"slow_down\" in errorMsg -> {\n                            consecutiveNetworkErrors = 0\n                            consecutiveUnknownErrors = 0\n                            slowDownCount++\n                            pollingInterval += 5000\n\n                            logger.debug(\"⚠️ Rate limited. New interval: ${pollingInterval}ms (slowdown #$slowDownCount)\")\n\n                            if (slowDownCount > 10) {\n                                throw Exception(\n                                    \"GitHub is experiencing high traffic. Please wait a few minutes and try again.\",\n                                )\n                            }\n\n                            delay(pollingInterval + (0..3000).random())\n                        }\n\n                        \"access_denied\" in errorMsg -> {\n                            throw Exception(\n                                \"Authentication was denied. Please try again if this was a mistake.\",\n                            )\n                        }\n\n                        \"expired_token\" in errorMsg ||\n                            \"expired_device_code\" in errorMsg ||\n                            \"token_expired\" in errorMsg -> {\n                            throw Exception(\n                                \"Authorization code expired. Please try again.\",\n                            )\n                        }\n\n                        \"bad_verification_code\" in errorMsg ||\n                            \"incorrect_device_code\" in errorMsg -> {\n                            throw Exception(\n                                \"Invalid verification code. Please restart authentication.\",\n                            )\n                        }\n\n                        isNetworkError(errorMsg) -> {\n                            consecutiveNetworkErrors++\n                            consecutiveUnknownErrors = 0\n\n                            logger.debug(\"⚠️ Network error ($consecutiveNetworkErrors/8): $errorMsg\")\n\n                            if (consecutiveNetworkErrors >= 8) {\n                                throw Exception(\n                                    \"Network connection is unstable. Please check your connection and try again.\",\n                                )\n                            }\n\n                            val backoff =\n                                minOf(\n                                    pollingInterval * (1 + consecutiveNetworkErrors),\n                                    30_000L,\n                                )\n                            delay(backoff)\n                        }\n\n                        else -> {\n                            consecutiveUnknownErrors++\n                            logger.debug(\"⚠️ Unknown error ($consecutiveUnknownErrors/5): $errorMsg\")\n\n                            if (consecutiveUnknownErrors >= 5) {\n                                throw Exception(\n                                    \"Authentication failed: ${error?.message ?: \"Unknown error\"}\",\n                                )\n                            }\n\n                            val backoff =\n                                minOf(\n                                    pollingInterval * (1 + consecutiveUnknownErrors / 2),\n                                    20_000L,\n                                )\n                            delay(backoff)\n                        }\n                    }\n                } catch (e: CancellationException) {\n                    throw e\n                } catch (e: TimeoutException) {\n                    throw e\n                } catch (e: Exception) {\n                    consecutiveUnknownErrors++\n                    logger.debug(\"❌ Unexpected error ($consecutiveUnknownErrors/5): ${e.message}\")\n\n                    if (consecutiveUnknownErrors >= 5) {\n                        throw Exception(\n                            \"Authentication failed after multiple errors: ${e.message}\",\n                            e,\n                        )\n                    }\n\n                    delay(minOf(pollingInterval * 2, 15_000L))\n                }\n            }\n\n            throw CancellationException(\"Authentication was cancelled\")\n        }\n\n    private suspend fun saveTokenWithVerification(token: GithubDeviceTokenSuccess) {\n        repeat(5) { attempt ->\n            try {\n                tokenStore.save(token.toData())\n\n                delay(100)\n                val saved = tokenStore.currentToken()\n\n                if (saved?.accessToken == token.accessToken) {\n                    return\n                } else {\n                    logger.debug(\"⚠️ Token verification failed (attempt ${attempt + 1}/5)\")\n                    if (attempt == 4) {\n                        throw Exception(\"Token was not persisted correctly after 5 attempts\")\n                    }\n                }\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                logger.debug(\"⚠️ Token save failed (attempt ${attempt + 1}/5): ${e.message}\")\n                if (attempt == 4) {\n                    throw Exception(\"Failed to save authentication token: ${e.message}\", e)\n                }\n                delay(500L * (attempt + 1))\n            }\n        }\n    }\n\n    private fun isNetworkError(errorMsg: String): Boolean =\n        errorMsg.contains(\"unable to resolve\") ||\n            errorMsg.contains(\"no address\") ||\n            errorMsg.contains(\"failed to connect\") ||\n            errorMsg.contains(\"connection refused\") ||\n            errorMsg.contains(\"network is unreachable\") ||\n            errorMsg.contains(\"timeout\") ||\n            errorMsg.contains(\"timed out\") ||\n            errorMsg.contains(\"connection reset\") ||\n            errorMsg.contains(\"broken pipe\") ||\n            errorMsg.contains(\"host unreachable\") ||\n            errorMsg.contains(\"network error\")\n}\n"
  },
  {
    "path": "feature/auth/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/auth/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/auth/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/auth/domain/src/commonMain/kotlin/zed/rainxch/auth/domain/repository/AuthenticationRepository.kt",
    "content": "package zed.rainxch.auth.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.GithubDeviceStart\nimport zed.rainxch.core.domain.model.GithubDeviceTokenSuccess\n\ninterface AuthenticationRepository {\n    val accessTokenFlow: Flow<String?>\n\n    suspend fun startDeviceFlow(): GithubDeviceStart\n\n    suspend fun awaitDeviceToken(start: GithubDeviceStart): GithubDeviceTokenSuccess\n}\n"
  },
  {
    "path": "feature/auth/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/auth/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.auth.domain)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/auth/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationAction.kt",
    "content": "package zed.rainxch.auth.presentation\n\nimport zed.rainxch.auth.presentation.model.GithubDeviceStartUi\n\nsealed interface AuthenticationAction {\n    data object StartLogin : AuthenticationAction\n\n    data class CopyCode(\n        val start: GithubDeviceStartUi,\n    ) : AuthenticationAction\n\n    data class OpenGitHub(\n        val start: GithubDeviceStartUi,\n    ) : AuthenticationAction\n\n    data object MarkLoggedOut : AuthenticationAction\n\n    data object MarkLoggedIn : AuthenticationAction\n\n    data class OnInfo(\n        val message: String,\n    ) : AuthenticationAction\n\n    data object SkipLogin : AuthenticationAction\n}\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationEvents.kt",
    "content": "package zed.rainxch.auth.presentation\n\nsealed interface AuthenticationEvents {\n    data object OnNavigateToMain : AuthenticationEvents\n}\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt",
    "content": "package zed.rainxch.auth.presentation\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ContentCopy\nimport androidx.compose.material.icons.filled.DoneAll\nimport androidx.compose.material.icons.filled.OpenWith\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.auth.presentation.model.AuthLoginState\nimport zed.rainxch.auth.presentation.model.GithubDeviceStartUi\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.app_icon\nimport zed.rainxch.githubstore.core.presentation.res.auth_code_expires_in\nimport zed.rainxch.githubstore.core.presentation.res.auth_error_with_message\nimport zed.rainxch.githubstore.core.presentation.res.continue_as_guest\nimport zed.rainxch.githubstore.core.presentation.res.copy_code\nimport zed.rainxch.githubstore.core.presentation.res.enter_code_on_github\nimport zed.rainxch.githubstore.core.presentation.res.ic_github\nimport zed.rainxch.githubstore.core.presentation.res.more_requests\nimport zed.rainxch.githubstore.core.presentation.res.more_requests_description\nimport zed.rainxch.githubstore.core.presentation.res.open_github\nimport zed.rainxch.githubstore.core.presentation.res.redirecting_message\nimport zed.rainxch.githubstore.core.presentation.res.sign_in_with_github\nimport zed.rainxch.githubstore.core.presentation.res.signed_in\nimport zed.rainxch.githubstore.core.presentation.res.try_again\nimport zed.rainxch.githubstore.core.presentation.res.unlock_full_experience\nimport zed.rainxch.githubstore.core.presentation.res.waiting_for_authorization\n\n@Composable\nfun AuthenticationRoot(\n    onNavigateToHome: () -> Unit,\n    viewModel: AuthenticationViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n\n    ObserveAsEvents(viewModel.events) { event ->\n        when (event) {\n            AuthenticationEvents.OnNavigateToMain -> {\n                onNavigateToHome()\n            }\n        }\n    }\n\n    AuthenticationScreen(\n        state = state,\n        onAction = viewModel::onAction,\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun AuthenticationScreen(\n    state: AuthenticationState,\n    onAction: (AuthenticationAction) -> Unit,\n) {\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        containerColor = MaterialTheme.colorScheme.background,\n    ) { innerPadding ->\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding)\n                    .padding(vertical = 32.dp, horizontal = 16.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Image(\n                painter = painterResource(Res.drawable.app_icon),\n                contentDescription = null,\n                modifier =\n                    Modifier\n                        .size(150.dp)\n                        .clip(RoundedCornerShape(32.dp)),\n                contentScale = ContentScale.Crop,\n            )\n\n            Spacer(Modifier.height(16.dp))\n\n            when (val authState = state.loginState) {\n                is AuthLoginState.LoggedOut -> {\n                    StateLoggedOut(\n                        onAction = onAction,\n                    )\n                }\n\n                is AuthLoginState.DevicePrompt -> {\n                    StateDevicePrompt(\n                        state = state,\n                        authState = authState,\n                        onAction = onAction,\n                    )\n                }\n\n                is AuthLoginState.Pending -> {\n                    CircularWavyProgressIndicator()\n\n                    Spacer(Modifier.height(12.dp))\n\n                    Text(\n                        text = stringResource(Res.string.waiting_for_authorization),\n                    )\n                }\n\n                is AuthLoginState.LoggedIn -> {\n                    Text(\n                        text = stringResource(Res.string.signed_in),\n                        style = MaterialTheme.typography.titleLarge,\n                    )\n\n                    Spacer(Modifier.height(8.dp))\n\n                    Text(\n                        text = stringResource(Res.string.redirecting_message),\n                    )\n                }\n\n                is AuthLoginState.Error -> {\n                    Spacer(Modifier.weight(1f))\n                    Text(\n                        text =\n                            stringResource(\n                                Res.string.auth_error_with_message,\n                                authState.message,\n                            ),\n                        style = MaterialTheme.typography.titleLarge,\n                        color = MaterialTheme.colorScheme.error,\n                    )\n\n                    authState.recoveryHint?.let { hint ->\n                        Spacer(Modifier.height(8.dp))\n                        Text(\n                            text = hint,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.primary,\n                            textAlign = TextAlign.Center,\n                        )\n                    }\n\n                    Spacer(Modifier.height(12.dp))\n\n                    GithubStoreButton(\n                        text = stringResource(Res.string.try_again),\n                        onClick = {\n                            onAction(AuthenticationAction.StartLogin)\n                        },\n                        modifier = Modifier.fillMaxWidth(.7f),\n                    )\n\n                    Spacer(Modifier.height(8.dp))\n\n                    TextButton(\n                        onClick = { onAction(AuthenticationAction.SkipLogin) },\n                    ) {\n                        Text(\n                            text = stringResource(Res.string.continue_as_guest),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.outline,\n                        )\n                    }\n\n                    Spacer(Modifier.weight(2f))\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun StateDevicePrompt(\n    state: AuthenticationState,\n    authState: AuthLoginState.DevicePrompt,\n    onAction: (AuthenticationAction) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Spacer(Modifier.weight(1f))\n\n        Text(\n            text = stringResource(Res.string.enter_code_on_github),\n            style = MaterialTheme.typography.titleLarge,\n            color = MaterialTheme.colorScheme.onBackground,\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp),\n        ) {\n            Text(\n                text = authState.start.userCode,\n                style = MaterialTheme.typography.headlineMedium,\n                fontWeight = FontWeight.Bold,\n            )\n\n            IconButton(\n                shapes = IconButtonDefaults.shapes(),\n                onClick = {\n                    onAction(AuthenticationAction.CopyCode(authState.start))\n                },\n                colors =\n                    IconButtonDefaults.iconButtonColors(\n                        containerColor = MaterialTheme.colorScheme.primaryContainer,\n                        contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                    ),\n            ) {\n                Icon(\n                    imageVector =\n                        if (state.copied) {\n                            Icons.Default.DoneAll\n                        } else {\n                            Icons.Default.ContentCopy\n                        },\n                    contentDescription = stringResource(Res.string.copy_code),\n                )\n            }\n        }\n\n        Spacer(Modifier.height(16.dp))\n\n        state.info?.let { info ->\n            Text(\n                text = info,\n                style = MaterialTheme.typography.headlineMedium,\n                fontWeight = FontWeight.Medium,\n                color = MaterialTheme.colorScheme.primary,\n            )\n        }\n\n        Spacer(Modifier.height(16.dp))\n\n        if (authState.remainingSeconds > 0) {\n            val minutes = authState.remainingSeconds / 60\n            val seconds = authState.remainingSeconds % 60\n            val formatted =\n                remember(minutes, seconds) {\n                    \"%02d:%02d\".format(minutes, seconds)\n                }\n            Text(\n                text = stringResource(Res.string.auth_code_expires_in, formatted),\n                style = MaterialTheme.typography.bodyMedium,\n                color =\n                    if (authState.remainingSeconds < 60) {\n                        MaterialTheme.colorScheme.error\n                    } else {\n                        MaterialTheme.colorScheme.outline\n                    },\n            )\n\n            Spacer(Modifier.height(16.dp))\n        }\n\n        GithubStoreButton(\n            text = stringResource(Res.string.open_github),\n            onClick = {\n                onAction(AuthenticationAction.OpenGitHub(authState.start))\n            },\n            icon = {\n                Icon(\n                    painter = painterResource(Res.drawable.ic_github),\n                    contentDescription = null,\n                    modifier = Modifier.size(24.dp),\n                )\n            },\n        )\n\n        Spacer(Modifier.weight(2f))\n    }\n}\n\n@Composable\nfun StateLoggedOut(onAction: (AuthenticationAction) -> Unit) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Text(\n            text = stringResource(Res.string.unlock_full_experience),\n            style = MaterialTheme.typography.headlineMedium,\n            color = MaterialTheme.colorScheme.onBackground,\n            textAlign = TextAlign.Center,\n        )\n\n        Spacer(Modifier.height(32.dp))\n\n        Card(\n            border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary),\n            modifier = Modifier.fillMaxWidth(),\n            shape = RoundedCornerShape(24.dp),\n        ) {\n            Column(\n                modifier = Modifier.padding(16.dp),\n                verticalArrangement = Arrangement.spacedBy(4.dp),\n            ) {\n                Icon(\n                    imageVector = Icons.Default.OpenWith,\n                    contentDescription = null,\n                    modifier = Modifier.size(24.dp),\n                    tint = MaterialTheme.colorScheme.primary,\n                )\n\n                Spacer(Modifier.height(4.dp))\n\n                Text(\n                    text = stringResource(Res.string.more_requests),\n                    style = MaterialTheme.typography.titleLarge,\n                    fontWeight = FontWeight.Bold,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n\n                Text(\n                    text = stringResource(Res.string.more_requests_description),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n            }\n        }\n\n        Spacer(Modifier.weight(1f))\n\n        GithubStoreButton(\n            text = stringResource(Res.string.sign_in_with_github),\n            onClick = {\n                onAction(AuthenticationAction.StartLogin)\n            },\n            icon = {\n                Icon(\n                    painter = painterResource(Res.drawable.ic_github),\n                    contentDescription = null,\n                    modifier = Modifier.size(24.dp),\n                )\n            },\n            modifier = Modifier.fillMaxWidth(),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        TextButton(\n            onClick = { onAction(AuthenticationAction.SkipLogin) },\n        ) {\n            Text(\n                text = stringResource(Res.string.continue_as_guest),\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.outline,\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        AuthenticationScreen(\n            state =\n                AuthenticationState(\n                    loginState =\n                        AuthLoginState.Error(\n                            message = \"Halo\",\n                        ),\n                ),\n            onAction = {},\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview1() {\n    GithubStoreTheme {\n        AuthenticationScreen(\n            state =\n                AuthenticationState(\n                    loginState = AuthLoginState.LoggedOut,\n                ),\n            onAction = {},\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview2() {\n    GithubStoreTheme {\n        AuthenticationScreen(\n            state =\n                AuthenticationState(\n                    loginState =\n                        AuthLoginState.DevicePrompt(\n                            GithubDeviceStartUi(\n                                deviceCode = \"\",\n                                userCode = \"2102-UHHUF\",\n                                verificationUri = \"\",\n                                expiresInSec = 10,\n                            ),\n                        ),\n                ),\n            onAction = {},\n        )\n    }\n}\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationState.kt",
    "content": "package zed.rainxch.auth.presentation\n\nimport zed.rainxch.auth.presentation.model.AuthLoginState\n\ndata class AuthenticationState(\n    val loginState: AuthLoginState = AuthLoginState.LoggedOut,\n    val copied: Boolean = false,\n    val info: String? = null,\n)\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationViewModel.kt",
    "content": "package zed.rainxch.auth.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.auth.domain.repository.AuthenticationRepository\nimport zed.rainxch.auth.presentation.mapper.toUi\nimport zed.rainxch.auth.presentation.model.AuthLoginState\nimport zed.rainxch.auth.presentation.model.GithubDeviceStartUi\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.GithubDeviceStart\nimport zed.rainxch.core.domain.utils.BrowserHelper\nimport zed.rainxch.core.domain.utils.ClipboardHelper\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nclass AuthenticationViewModel(\n    private val authenticationRepository: AuthenticationRepository,\n    private val browserHelper: BrowserHelper,\n    private val clipboardHelper: ClipboardHelper,\n    private val scope: CoroutineScope,\n    private val logger: GitHubStoreLogger,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n    private var countdownJob: Job? = null\n\n    private val _state: MutableStateFlow<AuthenticationState> =\n        MutableStateFlow(AuthenticationState())\n\n    private val _events = Channel<AuthenticationEvents>(capacity = Channel.BUFFERED)\n    val events = _events.receiveAsFlow()\n\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    scope.launch {\n                        authenticationRepository.accessTokenFlow.collect { token ->\n                            _state.update {\n                                it.copy(\n                                    loginState =\n                                        if (token.isNullOrEmpty()) {\n                                            AuthLoginState.LoggedOut\n                                        } else {\n                                            _events.trySend(AuthenticationEvents.OnNavigateToMain)\n                                            AuthLoginState.LoggedIn\n                                        },\n                                )\n                            }\n                        }\n                    }\n\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = AuthenticationState(),\n            )\n\n    fun onAction(action: AuthenticationAction) {\n        when (action) {\n            is AuthenticationAction.StartLogin -> {\n                startLogin()\n            }\n\n            is AuthenticationAction.CopyCode -> {\n                copyCode(action.start)\n            }\n\n            is AuthenticationAction.OpenGitHub -> {\n                openGitHub(action.start)\n            }\n\n            AuthenticationAction.MarkLoggedIn -> {\n                _state.update { it.copy(loginState = AuthLoginState.LoggedIn) }\n            }\n\n            AuthenticationAction.MarkLoggedOut -> {\n                _state.update { it.copy(loginState = AuthLoginState.LoggedOut) }\n            }\n\n            is AuthenticationAction.OnInfo -> {\n                _state.update {\n                    it.copy(\n                        info = action.message,\n                    )\n                }\n            }\n\n            AuthenticationAction.SkipLogin -> {\n                _events.trySend(AuthenticationEvents.OnNavigateToMain)\n            }\n        }\n    }\n\n    private fun startCountdown(start: GithubDeviceStart) {\n        countdownJob?.cancel()\n        countdownJob =\n            viewModelScope.launch {\n                var remaining = start.expiresInSec\n                while (remaining > 0) {\n                    _state.update { currentState ->\n                        val loginState = currentState.loginState\n                        if (loginState is AuthLoginState.DevicePrompt) {\n                            currentState.copy(\n                                loginState = loginState.copy(remainingSeconds = remaining),\n                            )\n                        } else {\n                            return@launch\n                        }\n                    }\n                    delay(1000L)\n                    remaining--\n                }\n                _state.update {\n                    it.copy(\n                        loginState =\n                            AuthLoginState.Error(\n                                message = getString(Res.string.auth_error_code_expired),\n                                recoveryHint = getString(Res.string.auth_hint_try_again),\n                            ),\n                    )\n                }\n            }\n    }\n\n    private fun startLogin() {\n        viewModelScope.launch {\n            try {\n                val start =\n                    withContext(Dispatchers.IO) {\n                        authenticationRepository.startDeviceFlow()\n                    }\n\n                withContext(Dispatchers.Main.immediate) {\n                    _state.update {\n                        it.copy(\n                            loginState =\n                                AuthLoginState.DevicePrompt(\n                                    start = start.toUi(),\n                                    remainingSeconds = start.expiresInSec,\n                                ),\n                            copied = false,\n                        )\n                    }\n\n                    startCountdown(start)\n\n                    try {\n                        clipboardHelper.copy(\n                            label = getString(Res.string.enter_code_on_github),\n                            text = start.userCode,\n                        )\n                        _state.update { it.copy(copied = true) }\n                    } catch (e: Exception) {\n                        logger.debug(\"Failed to copy to clipboard: ${e.message}\")\n                    }\n                }\n\n                withContext(Dispatchers.IO) {\n                    authenticationRepository.awaitDeviceToken(start = start)\n                }\n\n                countdownJob?.cancel()\n\n                withContext(Dispatchers.Main.immediate) {\n                    _state.update { it.copy(loginState = AuthLoginState.LoggedIn) }\n                    _events.trySend(AuthenticationEvents.OnNavigateToMain)\n                }\n            } catch (e: CancellationException) {\n                throw e\n            } catch (t: Throwable) {\n                countdownJob?.cancel()\n                val (message, hint) = categorizeError(t)\n                withContext(Dispatchers.Main.immediate) {\n                    _state.update {\n                        it.copy(\n                            loginState =\n                                AuthLoginState.Error(\n                                    message = message,\n                                    recoveryHint = hint,\n                                ),\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    private suspend fun categorizeError(t: Throwable): Pair<String, String?> {\n        val msg = t.message ?: return getString(Res.string.error_unknown) to null\n        val lowerMsg = msg.lowercase()\n        return when {\n            \"timeout\" in lowerMsg || \"timed out\" in lowerMsg -> {\n                msg to getString(Res.string.auth_hint_check_connection)\n            }\n\n            \"network\" in lowerMsg || \"unresolvedaddress\" in lowerMsg || \"connect\" in lowerMsg -> {\n                msg to getString(Res.string.auth_hint_check_connection)\n            }\n\n            \"expired\" in lowerMsg || \"expire\" in lowerMsg -> {\n                msg to getString(Res.string.auth_hint_try_again)\n            }\n\n            \"denied\" in lowerMsg || \"access_denied\" in lowerMsg -> {\n                msg to getString(Res.string.auth_hint_denied)\n            }\n\n            else -> {\n                msg to null\n            }\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        countdownJob?.cancel()\n    }\n\n    private fun openGitHub(start: GithubDeviceStartUi) {\n        viewModelScope.launch(Dispatchers.Main.immediate) {\n            try {\n                val url = start.verificationUriComplete ?: start.verificationUri\n                browserHelper.openUrl(url)\n            } catch (e: Exception) {\n                logger.debug(\"⚠️ Failed to open browser: ${e.message}\")\n            }\n        }\n    }\n\n    private fun copyCode(start: GithubDeviceStartUi) {\n        viewModelScope.launch(Dispatchers.Main.immediate) {\n            try {\n                clipboardHelper.copy(\n                    label = \"GitHub Code\",\n                    text = start.userCode,\n                )\n\n                _state.update {\n                    val currentRemaining = (it.loginState as? AuthLoginState.DevicePrompt)?.remainingSeconds ?: 0\n\n                    it.copy(\n                        loginState = AuthLoginState.DevicePrompt(start, currentRemaining),\n                        copied = true,\n                    )\n                }\n            } catch (e: Exception) {\n                logger.debug(\"⚠️ Failed to copy to clipboard: ${e.message}\")\n                _state.update {\n                    val currentRemaining = (it.loginState as? AuthLoginState.DevicePrompt)?.remainingSeconds ?: 0\n\n                    it.copy(\n                        loginState = AuthLoginState.DevicePrompt(start, currentRemaining),\n                        copied = false,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/mapper/GithubDeviceStartMapper.kt",
    "content": "package zed.rainxch.auth.presentation.mapper\n\nimport zed.rainxch.auth.presentation.model.GithubDeviceStartUi\nimport zed.rainxch.core.domain.model.GithubDeviceStart\n\nfun GithubDeviceStart.toUi(): GithubDeviceStartUi =\n    GithubDeviceStartUi(\n        deviceCode = deviceCode,\n        userCode = userCode,\n        verificationUri = verificationUri,\n        verificationUriComplete = verificationUriComplete,\n        intervalSec = intervalSec,\n        expiresInSec = expiresInSec,\n    )\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/model/AuthLoginState.kt",
    "content": "package zed.rainxch.auth.presentation.model\n\nsealed class AuthLoginState {\n    data object LoggedOut : AuthLoginState()\n\n    data class DevicePrompt(\n        val start: GithubDeviceStartUi,\n        val remainingSeconds: Int = 0,\n    ) : AuthLoginState()\n\n    data object Pending : AuthLoginState()\n\n    data object LoggedIn : AuthLoginState()\n\n    data class Error(\n        val message: String,\n        val recoveryHint: String? = null,\n    ) : AuthLoginState()\n}\n"
  },
  {
    "path": "feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/model/GithubDeviceStartUi.kt",
    "content": "package zed.rainxch.auth.presentation.model\n\ndata class GithubDeviceStartUi(\n    val deviceCode: String,\n    val userCode: String,\n    val verificationUri: String,\n    val verificationUriComplete: String? = null,\n    val intervalSec: Int = 5,\n    val expiresInSec: Int,\n)\n"
  },
  {
    "path": "feature/details/CLAUDE.md",
    "content": "# CLAUDE.md - Details Feature\n\n## Purpose\n\nRepository detail screen. Displays full info for a GitHub repository including owner profile, stats, releases with download links, readme rendering (with translation support), and installation/update flow. This is the most complex feature module.\n\n## Module Structure\n\n```\nfeature/details/\n├── domain/\n│   ├── model/\n│   │   ├── ReleaseCategory.kt        # Release filtering categories\n│   │   ├── RepoStats.kt              # Stars, forks, open issues\n│   │   ├── SupportedLanguage.kt      # Languages for readme translation\n│   │   └── TranslationResult.kt      # Translation response model\n│   └── repository/\n│       ├── DetailsRepository.kt      # Repo, releases, readme, stats, user profile\n│       └── TranslationRepository.kt  # Readme translation\n├── data/\n│   ├── di/SharedModule.kt            # Koin: detailsModule\n│   ├── repository/\n│   │   ├── DetailsRepositoryImpl.kt  # API calls + readme localization\n│   │   └── TranslationRepositoryImpl.kt  # Translation API integration\n│   ├── model/ReadmeAttempt.kt        # Readme fetch attempt tracking\n│   └── utils/\n│       ├── ReadmeLocalizationHelper.kt   # Find readme in user's language\n│       └── preprocessMarkdown.kt     # Markdown preprocessing\n└── presentation/\n    ├── DetailsViewModel.kt            # State management for detail screen\n    ├── DetailsState.kt                # Repo, releases, readme, download progress, etc.\n    ├── DetailsAction.kt               # Load, download, install, favourite, star, etc.\n    ├── DetailsEvent.kt                # Navigation, toast events\n    ├── DetailsRoot.kt                 # Main composable\n    ├── model/\n    │   ├── DownloadStage.kt           # Download progress tracking\n    │   ├── InstallLogItem.kt          # Installation log entries\n    │   ├── LogResult.kt               # Log result types\n    │   ├── ShowDowngradeWarning.kt    # Downgrade confirmation model\n    │   ├── SupportedLanguages.kt      # UI language list\n    │   ├── TranslationState.kt        # Translation UI state\n    │   └── TranslationTarget.kt       # Translation target selection\n    ├── components/\n    │   ├── AppHeader.kt               # App icon, name, developer\n    │   ├── LanguagePicker.kt          # Readme translation language selector\n    │   ├── ReleaseAssetsPicker.kt     # Asset selection for download\n    │   ├── SmartInstallButton.kt      # Context-aware install/update/open button\n    │   ├── StatItem.kt                # Individual stat display\n    │   ├── TranslationControls.kt     # Translation UI controls\n    │   ├── VersionPicker.kt           # Release version selector\n    │   ├── VersionTypePicker.kt       # Stable/pre-release filter\n    │   └── sections/\n    │       ├── About.kt               # Description & topics\n    │       ├── Header.kt              # Top header section\n    │       ├── Logs.kt                # Installation/download logs\n    │       ├── Owner.kt               # Repository owner info\n    │       ├── ReportIssue.kt         # Issue reporting section\n    │       ├── Stats.kt               # Stars, forks, issues\n    │       └── WhatsNew.kt            # Release changelog\n    ├── states/ErrorState.kt           # Error display composable\n    └── utils/\n        ├── LocalTopbarLiquidState.kt\n        ├── LogResultAsText.kt         # Log result formatting\n        ├── MarkdownImageTransformer.kt  # Transform relative image URLs\n        ├── MarkdownUtils.kt           # Markdown preprocessing\n        └── SystemArchitecture.kt      # Platform architecture detection\n```\n\n## Key Interfaces\n\n```kotlin\ninterface DetailsRepository {\n    suspend fun getRepositoryById(id: Long): GithubRepoSummary\n    suspend fun getRepositoryByOwnerAndName(owner: String, name: String): GithubRepoSummary\n    suspend fun getLatestPublishedRelease(owner: String, repo: String, defaultBranch: String): GithubRelease?\n    suspend fun getAllReleases(owner: String, repo: String, defaultBranch: String): List<GithubRelease>\n    suspend fun getReadme(owner: String, repo: String, defaultBranch: String): Triple<ReadmeContent, LanguageCode?, ReadmePath>?\n    suspend fun getRepoStats(owner: String, repo: String): RepoStats\n    suspend fun getUserProfile(username: String): GithubUserProfile\n}\n\ninterface TranslationRepository {\n    suspend fun translate(text: String, targetLanguage: SupportedLanguage): TranslationResult\n}\n```\n\n## Navigation\n\nRoute: `GithubStoreGraph.DetailsScreen(repositoryId: Long, owner: String, repo: String, isComingFromUpdate: Boolean)`\n\nCan be reached via repo ID or owner+name (for deep links). Falls back to owner+name lookup if `repositoryId == -1`. `isComingFromUpdate` flag indicates navigation from an update notification.\n\n## Implementation Notes\n\n- Readme supports localization: `ReadmeLocalizationHelper` tries to find readme in user's language first\n- Readme translation: `TranslationRepository` translates readme content to user's chosen language via `LanguagePicker`\n- Markdown rendering uses `multiplatform-markdown-renderer` with custom `MarkdownImageTransformer` for relative URLs\n- Download flow tracks stages via `DownloadStage` (idle → downloading → installing → done)\n- `SmartInstallButton` changes behavior based on installed/update-available/not-installed state\n- `ReleaseAssetsPicker` allows selecting specific assets; `VersionTypePicker` filters stable vs pre-release\n- Version picker allows selecting specific releases for download\n- Downgrade warning shown when installing an older version than currently installed\n- Integrates with `FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository` from core\n- Uses `Downloader` and `Installer` interfaces from core/domain for platform-specific download/install\n- On Android, install may use Shizuku (silent) or standard system installer depending on user preference in profile settings\n"
  },
  {
    "path": "feature/details/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/details/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.details.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt",
    "content": "package zed.rainxch.details.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.details.data.repository.DetailsRepositoryImpl\nimport zed.rainxch.details.data.repository.TranslationRepositoryImpl\nimport zed.rainxch.details.domain.repository.DetailsRepository\nimport zed.rainxch.details.domain.repository.TranslationRepository\n\nval detailsModule =\n    module {\n        single<DetailsRepository> {\n            DetailsRepositoryImpl(\n                logger = get(),\n                httpClient = get(),\n                localizationManager = get(),\n                cacheManager = get(),\n            )\n        }\n\n        single<TranslationRepository> {\n            TranslationRepositoryImpl(\n                localizationManager = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/dto/AttestationsResponse.kt",
    "content": "package zed.rainxch.details.data.dto\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class AttestationsResponse(\n    val attestations: List<JsonObject> = emptyList(),\n)\n"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/model/ReadmeAttempt.kt",
    "content": "package zed.rainxch.details.data.model\n\ndata class ReadmeAttempt(\n    val path: String,\n    val filename: String,\n    val priority: Int,\n)\n"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt",
    "content": "package zed.rainxch.details.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.http.HttpHeaders\nimport kotlinx.serialization.Serializable\nimport zed.rainxch.core.data.cache.CacheManager\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.README\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.RELEASES\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.REPO_DETAILS\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.REPO_STATS\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.USER_PROFILE\nimport zed.rainxch.core.data.dto.ReleaseNetwork\nimport zed.rainxch.core.data.dto.RepoByIdNetwork\nimport zed.rainxch.core.data.dto.RepoInfoNetwork\nimport zed.rainxch.core.data.dto.UserProfileNetwork\nimport zed.rainxch.details.data.dto.AttestationsResponse\nimport zed.rainxch.core.data.mappers.toDomain\nimport zed.rainxch.core.data.network.executeRequest\nimport zed.rainxch.core.data.services.LocalizationManager\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.GithubUser\nimport zed.rainxch.core.domain.model.GithubUserProfile\nimport zed.rainxch.details.data.utils.ReadmeLocalizationHelper\nimport zed.rainxch.details.data.utils.preprocessMarkdown\nimport zed.rainxch.details.domain.model.RepoStats\nimport zed.rainxch.details.domain.repository.DetailsRepository\n\nclass DetailsRepositoryImpl(\n    private val httpClient: HttpClient,\n    private val localizationManager: LocalizationManager,\n    private val logger: GitHubStoreLogger,\n    private val cacheManager: CacheManager,\n) : DetailsRepository {\n    @Serializable\n    private data class CachedReadme(\n        val content: String,\n        val languageCode: String?,\n        val path: String,\n    )\n\n    private val readmeHelper = ReadmeLocalizationHelper(localizationManager)\n\n    private fun RepoByIdNetwork.toGithubRepoSummary(): GithubRepoSummary =\n        GithubRepoSummary(\n            id = id,\n            name = name,\n            fullName = fullName,\n            owner =\n                GithubUser(\n                    id = owner.id,\n                    login = owner.login,\n                    avatarUrl = owner.avatarUrl,\n                    htmlUrl = owner.htmlUrl,\n                ),\n            description = description,\n            htmlUrl = htmlUrl,\n            stargazersCount = stars,\n            forksCount = forks,\n            language = language,\n            topics = topics,\n            releasesUrl = \"https://api.github.com/repos/${owner.login}/$name/releases{/id}\",\n            updatedAt = updatedAt,\n            defaultBranch = defaultBranch,\n        )\n\n    override suspend fun getRepositoryById(id: Long): GithubRepoSummary {\n        val cacheKey = \"details:repo_id:$id\"\n\n        cacheManager.get<GithubRepoSummary>(cacheKey)?.let { cached ->\n            logger.debug(\"Cache hit for repo id=$id\")\n            return cached\n        }\n\n        return try {\n            val result =\n                httpClient\n                    .executeRequest<RepoByIdNetwork> {\n                        get(\"/repositories/$id\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                        }\n                    }.getOrThrow()\n                    .toGithubRepoSummary()\n            cacheManager.put(cacheKey, result, REPO_DETAILS)\n            result\n        } catch (e: Exception) {\n            cacheManager.getStale<GithubRepoSummary>(cacheKey)?.let { stale ->\n                logger.debug(\"Network error, using stale cache for repo id=$id\")\n                return stale\n            }\n            throw e\n        }\n    }\n\n    override suspend fun getRepositoryByOwnerAndName(\n        owner: String,\n        name: String,\n    ): GithubRepoSummary {\n        val cacheKey = \"details:repo:$owner/$name\"\n\n        cacheManager.get<GithubRepoSummary>(cacheKey)?.let { cached ->\n            logger.debug(\"Cache hit for repo $owner/$name\")\n            return cached\n        }\n\n        return try {\n            val result =\n                httpClient\n                    .executeRequest<RepoByIdNetwork> {\n                        get(\"/repos/$owner/$name\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                        }\n                    }.getOrThrow()\n                    .toGithubRepoSummary()\n\n            cacheManager.put(cacheKey, result, REPO_DETAILS)\n            result\n        } catch (e: Exception) {\n            cacheManager.getStale<GithubRepoSummary>(cacheKey)?.let { stale ->\n                logger.debug(\"Network error, using stale cache for $owner/$name\")\n                return stale\n            }\n            throw e\n        }\n    }\n\n    override suspend fun getLatestPublishedRelease(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): GithubRelease? {\n        val cacheKey = \"details:latest_release:$owner/$repo\"\n\n        cacheManager.get<GithubRelease>(cacheKey)?.let { cached ->\n            logger.debug(\"Cache hit for latest release $owner/$repo\")\n            return cached\n        }\n\n        return try {\n            val releases =\n                httpClient\n                    .executeRequest<List<ReleaseNetwork>> {\n                        get(\"/repos/$owner/$repo/releases\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                            parameter(\"per_page\", 10)\n                        }\n                    }.getOrNull() ?: return null\n\n            val latest =\n                releases\n                    .asSequence()\n                    .filter { (it.draft != true) && (it.prerelease != true) }\n                    .maxByOrNull { it.publishedAt ?: it.createdAt ?: \"\" }\n                    ?: return null\n\n            val result =\n                latest\n                    .copy(\n                        body = processReleaseBody(latest.body, owner, repo, defaultBranch),\n                    ).toDomain()\n\n            cacheManager.put(cacheKey, result, RELEASES)\n            result\n        } catch (e: Exception) {\n            cacheManager.getStale<GithubRelease>(cacheKey)?.let { stale ->\n                logger.debug(\"Network error, using stale cache for latest release $owner/$repo\")\n                return stale\n            }\n            throw e\n        }\n    }\n\n    override suspend fun getAllReleases(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): List<GithubRelease> {\n        val cacheKey = \"details:releases:$owner/$repo\"\n\n        cacheManager.get<List<GithubRelease>>(cacheKey)?.let { cached ->\n            if (cached.isNotEmpty()) {\n                logger.debug(\"Cache hit for all releases $owner/$repo: ${cached.size} releases\")\n                return cached\n            }\n        }\n\n        return try {\n            val releases =\n                httpClient\n                    .executeRequest<List<ReleaseNetwork>> {\n                        get(\"/repos/$owner/$repo/releases\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                            parameter(\"per_page\", 30)\n                        }\n                    }.getOrNull() ?: return emptyList()\n\n            val result =\n                releases\n                    .filter { it.draft != true }\n                    .map { release ->\n                        release\n                            .copy(\n                                body = processReleaseBody(release.body, owner, repo, defaultBranch),\n                            ).toDomain()\n                    }.sortedByDescending { it.publishedAt }\n\n            if (result.isNotEmpty()) {\n                cacheManager.put(cacheKey, result, RELEASES)\n            }\n            result\n        } catch (e: Exception) {\n            cacheManager.getStale<List<GithubRelease>>(cacheKey)?.let { stale ->\n                logger.debug(\"Network error, using stale cache for releases $owner/$repo\")\n                return stale\n            }\n            throw e\n        }\n    }\n\n    private fun processReleaseBody(\n        body: String?,\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): String? =\n        body\n            ?.replace(\"<details>\", \"\")\n            ?.replace(\"</details>\", \"\")\n            ?.replace(\"<summary>\", \"\")\n            ?.replace(\"</summary>\", \"\")\n            ?.replace(\"\\r\\n\", \"\\n\")\n            ?.let { rawMarkdown ->\n                preprocessMarkdown(\n                    markdown = rawMarkdown,\n                    baseUrl = \"https://raw.githubusercontent.com/$owner/$repo/$defaultBranch/\",\n                )\n            }\n\n    override suspend fun getReadme(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): Triple<String, String?, String>? {\n        val cacheKey = \"details:readme:$owner/$repo\"\n\n        cacheManager.get<CachedReadme>(cacheKey)?.let { cached ->\n            logger.debug(\"Cache hit for readme $owner/$repo\")\n            return Triple(cached.content, cached.languageCode, cached.path)\n        }\n\n        val result = fetchReadmeFromApi(owner, repo, defaultBranch)\n\n        if (result != null) {\n            val cachedReadme =\n                CachedReadme(\n                    content = result.first,\n                    languageCode = result.second,\n                    path = result.third,\n                )\n            cacheManager.put(cacheKey, cachedReadme, README)\n        }\n\n        return result\n    }\n\n    private suspend fun fetchReadmeFromApi(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): Triple<String, String?, String>? {\n        val baseUrl = \"https://raw.githubusercontent.com/$owner/$repo/$defaultBranch/\"\n        val path = \"README.md\"\n\n        return try {\n            val rawMarkdown =\n                httpClient\n                    .executeRequest<String> {\n                        get(\"$baseUrl$path\")\n                    }.getOrNull()\n\n            if (rawMarkdown != null) {\n                val processed = preprocessMarkdown(markdown = rawMarkdown, baseUrl = baseUrl)\n                val detectedLang = readmeHelper.detectReadmeLanguage(processed)\n                logger.debug(\"Fetched README.md (detected language: ${detectedLang ?: \"unknown\"})\")\n                Triple(processed, detectedLang, path)\n            } else {\n                logger.error(\"Failed to fetch README.md for $owner/$repo\")\n                null\n            }\n        } catch (e: Throwable) {\n            logger.error(\"Failed to fetch README.md: ${e.message}\")\n            null\n        }\n    }\n\n    override suspend fun getRepoStats(\n        owner: String,\n        repo: String,\n    ): RepoStats {\n        val cacheKey = \"details:stats:$owner/$repo\"\n\n        cacheManager.get<RepoStats>(cacheKey)?.let { cached ->\n            logger.debug(\"Cache hit for repo stats $owner/$repo\")\n            return cached\n        }\n\n        return try {\n            val info =\n                httpClient\n                    .executeRequest<RepoInfoNetwork> {\n                        get(\"/repos/$owner/$repo\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                        }\n                    }.getOrThrow()\n\n            val result =\n                RepoStats(\n                    stars = info.stars,\n                    forks = info.forks,\n                    openIssues = info.openIssues,\n                )\n\n            cacheManager.put(cacheKey, result, REPO_STATS)\n            result\n        } catch (e: Exception) {\n            cacheManager.getStale<RepoStats>(cacheKey)?.let { stale ->\n                logger.debug(\"Network error, using stale cache for stats $owner/$repo\")\n                return stale\n            }\n            throw e\n        }\n    }\n\n    override suspend fun getUserProfile(username: String): GithubUserProfile {\n        val cacheKey = \"details:profile:$username\"\n\n        cacheManager.get<GithubUserProfile>(cacheKey)?.let { cached ->\n            logger.debug(\"Cache hit for user profile $username\")\n            return cached\n        }\n\n        return try {\n            val user =\n                httpClient\n                    .executeRequest<UserProfileNetwork> {\n                        get(\"/users/$username\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                        }\n                    }.getOrThrow()\n\n            val result =\n                GithubUserProfile(\n                    id = user.id,\n                    login = user.login,\n                    name = user.name,\n                    bio = user.bio,\n                    avatarUrl = user.avatarUrl,\n                    htmlUrl = user.htmlUrl,\n                    followers = user.followers,\n                    following = user.following,\n                    publicRepos = user.publicRepos,\n                    location = user.location,\n                    company = user.company,\n                    blog = user.blog,\n                    twitterUsername = user.twitterUsername,\n                )\n\n            cacheManager.put(cacheKey, result, USER_PROFILE)\n            result\n        } catch (e: Exception) {\n            cacheManager.getStale<GithubUserProfile>(cacheKey)?.let { stale ->\n                logger.debug(\"Network error, using stale cache for profile $username\")\n                return stale\n            }\n            throw e\n        }\n    }\n\n    override suspend fun checkAttestations(\n        owner: String,\n        repo: String,\n        sha256Digest: String,\n    ): Boolean =\n        try {\n            val response =\n                httpClient\n                    .executeRequest<AttestationsResponse> {\n                        get(\"/repos/$owner/$repo/attestations/sha256:$sha256Digest\") {\n                            header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                        }\n                    }.getOrNull()\n            response != null && response.attestations.isNotEmpty()\n        } catch (e: Exception) {\n            logger.debug(\"Attestation check failed for $owner/$repo: ${e.message}\")\n            false\n        }\n\n}\n"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt",
    "content": "package zed.rainxch.details.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonPrimitive\nimport zed.rainxch.core.data.network.createPlatformHttpClient\nimport zed.rainxch.core.data.services.LocalizationManager\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.details.domain.model.TranslationResult\nimport zed.rainxch.details.domain.repository.TranslationRepository\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nclass TranslationRepositoryImpl(\n    private val localizationManager: LocalizationManager,\n) : TranslationRepository {\n    private val httpClient: HttpClient = createPlatformHttpClient(ProxyConfig.None)\n\n    private val json =\n        Json {\n            ignoreUnknownKeys = true\n            isLenient = true\n        }\n\n    private val cacheMutex = Mutex()\n    private val cache = LinkedHashMap<CacheKey, CachedTranslation>(MAX_CACHE_SIZE, 0.75f, true)\n    private val maxChunkSize = 4500\n\n    @OptIn(ExperimentalTime::class)\n    override suspend fun translate(\n        text: String,\n        targetLanguage: String,\n        sourceLanguage: String,\n    ): TranslationResult {\n        val cacheKey = CacheKey(text, targetLanguage, sourceLanguage)\n\n        cacheMutex.withLock {\n            cache[cacheKey]?.let { cached ->\n                if (!cached.isExpired()) return cached.result\n                cache.remove(cacheKey)\n            }\n        }\n\n        val chunks = chunkText(text)\n        val translatedParts = mutableListOf<Pair<String, String>>()\n        var detectedLang: String? = null\n\n        for ((chunkText, delimiter) in chunks) {\n            val response = translateSingleChunk(chunkText, targetLanguage, sourceLanguage)\n            translatedParts.add(response.translatedText to delimiter)\n            if (detectedLang == null) {\n                detectedLang = response.detectedSourceLanguage\n            }\n        }\n\n        val result =\n            TranslationResult(\n                translatedText =\n                    translatedParts\n                        .dropLast(1)\n                        .joinToString(\"\") { (text, delim) -> text + delim } +\n                        translatedParts.lastOrNull()?.first.orEmpty(),\n                detectedSourceLanguage = detectedLang,\n            )\n\n        cacheMutex.withLock {\n            if (cache.size >= MAX_CACHE_SIZE) {\n                val firstKey = cache.keys.first()\n                cache.remove(firstKey)\n            }\n            cache[cacheKey] = CachedTranslation(result)\n        }\n        return result\n    }\n\n    override fun getDeviceLanguageCode(): String = localizationManager.getPrimaryLanguageCode()\n\n    private suspend fun translateSingleChunk(\n        text: String,\n        targetLanguage: String,\n        sourceLanguage: String,\n    ): TranslationResult {\n        val responseText =\n            httpClient\n                .get(\n                    \"https://translate.googleapis.com/translate_a/single\",\n                ) {\n                    parameter(\"client\", \"gtx\")\n                    parameter(\"sl\", sourceLanguage)\n                    parameter(\"tl\", targetLanguage)\n                    parameter(\"dt\", \"t\")\n                    parameter(\"q\", text)\n                }.bodyAsText()\n\n        return parseTranslationResponse(responseText)\n    }\n\n    private fun parseTranslationResponse(responseText: String): TranslationResult {\n        val root = json.parseToJsonElement(responseText).jsonArray\n\n        val segments = root[0].jsonArray\n        val translatedText =\n            segments.joinToString(\"\") { segment ->\n                segment.jsonArray[0].jsonPrimitive.content\n            }\n\n        val detectedLang =\n            try {\n                root[2].jsonPrimitive.content\n            } catch (_: Exception) {\n                null\n            }\n\n        return TranslationResult(\n            translatedText = translatedText,\n            detectedSourceLanguage = detectedLang,\n        )\n    }\n\n    private fun chunkText(text: String): List<Pair<String, String>> {\n        val paragraphs = text.split(\"\\n\\n\")\n        val chunks = mutableListOf<Pair<String, String>>()\n        val currentChunk = StringBuilder()\n\n        for (paragraph in paragraphs) {\n            if (paragraph.length > maxChunkSize) {\n                if (currentChunk.isNotEmpty()) {\n                    chunks.add(Pair(currentChunk.toString(), \"\\n\\n\"))\n                    currentChunk.clear()\n                }\n                chunkLargeParagraph(paragraph, chunks)\n            } else if (currentChunk.length + paragraph.length + 2 > maxChunkSize) {\n                chunks.add(Pair(currentChunk.toString(), \"\\n\\n\"))\n                currentChunk.clear()\n                currentChunk.append(paragraph)\n            } else {\n                if (currentChunk.isNotEmpty()) currentChunk.append(\"\\n\\n\")\n                currentChunk.append(paragraph)\n            }\n        }\n\n        if (currentChunk.isNotEmpty()) {\n            chunks.add(Pair(currentChunk.toString(), \"\\n\\n\"))\n        }\n\n        return chunks\n    }\n\n    private fun chunkLargeParagraph(\n        paragraph: String,\n        chunks: MutableList<Pair<String, String>>,\n    ) {\n        val lines = paragraph.split(\"\\n\")\n        val currentChunk = StringBuilder()\n\n        for (line in lines) {\n            if (line.length > maxChunkSize) {\n                if (currentChunk.isNotEmpty()) {\n                    chunks.add(Pair(currentChunk.toString(), \"\\n\"))\n                    currentChunk.clear()\n                }\n                var start = 0\n                while (start < line.length) {\n                    val end = minOf(start + maxChunkSize, line.length)\n                    chunks.add(Pair(line.substring(start, end), \"\"))\n                    start = end\n                }\n            } else if (currentChunk.length + line.length + 1 > maxChunkSize) {\n                chunks.add(Pair(currentChunk.toString(), \"\\n\"))\n                currentChunk.clear()\n                currentChunk.append(line)\n            } else {\n                if (currentChunk.isNotEmpty()) currentChunk.append(\"\\n\")\n                currentChunk.append(line)\n            }\n        }\n\n        if (currentChunk.isNotEmpty()) {\n            chunks.add(Pair(currentChunk.toString(), \"\\n\"))\n        }\n    }\n\n    companion object {\n        private const val MAX_CACHE_SIZE = 50\n        private const val CACHE_TTL_MS = 30 * 60 * 1000L // 30 minutes\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private class CachedTranslation(\n        val result: TranslationResult,\n        private val timestamp: Long = Clock.System.now().toEpochMilliseconds(),\n    ) {\n        fun isExpired(): Boolean = Clock.System.now().toEpochMilliseconds() - timestamp > CACHE_TTL_MS\n    }\n\n    private data class CacheKey(\n        val text: String,\n        val targetLanguage: String,\n        val sourceLanguage: String,\n    )\n}\n"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/ReadmeLocalizationHelper.kt",
    "content": "package zed.rainxch.details.data.utils\n\nimport zed.rainxch.details.data.model.ReadmeAttempt\n\nclass ReadmeLocalizationHelper(\n    private val localizationManager: zed.rainxch.core.data.services.LocalizationManager,\n) {\n    private val searchPaths =\n        listOf(\n            \".github\",\n            \"\",\n            \"docs\",\n            \"doc\",\n        )\n\n    fun generateReadmeAttempts(): List<ReadmeAttempt> {\n        val attempts = mutableListOf<ReadmeAttempt>()\n        val currentLang = localizationManager.getCurrentLanguageCode().lowercase()\n        val primaryLang = localizationManager.getPrimaryLanguageCode().lowercase()\n\n        var globalPriority = 0\n\n        for ((pathIndex, searchPath) in searchPaths.withIndex()) {\n            val pathPrefix = if (searchPath.isEmpty()) \"\" else \"$searchPath/\"\n\n            var localPriority = 0\n\n            if (currentLang.contains(\"-\")) {\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README.$currentLang.md\",\n                        filename = \"README.$currentLang.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README.${currentLang.replace(\"-\", \"_\")}.md\",\n                        filename = \"README.${currentLang.replace(\"-\", \"_\")}.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n            }\n\n            attempts.add(\n                ReadmeAttempt(\n                    path = \"${pathPrefix}README.$primaryLang.md\",\n                    filename = \"README.$primaryLang.md\",\n                    priority = globalPriority + localPriority++,\n                ),\n            )\n\n            if (currentLang.contains(\"-\")) {\n                val parts = currentLang.split(\"-\")\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README.${parts[0].uppercase()}.md\",\n                        filename = \"README.${parts[0].uppercase()}.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README-${parts[0].uppercase()}.md\",\n                        filename = \"README-${parts[0].uppercase()}.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n            } else {\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README.${primaryLang.uppercase()}.md\",\n                        filename = \"README.${primaryLang.uppercase()}.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README-${primaryLang.uppercase()}.md\",\n                        filename = \"README-${primaryLang.uppercase()}.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n            }\n\n            attempts.add(\n                ReadmeAttempt(\n                    path = \"${pathPrefix}README_$primaryLang.md\",\n                    filename = \"README_$primaryLang.md\",\n                    priority = globalPriority + localPriority++,\n                ),\n            )\n            attempts.add(\n                ReadmeAttempt(\n                    path = \"${pathPrefix}readme.$primaryLang.md\",\n                    filename = \"readme.$primaryLang.md\",\n                    priority = globalPriority + localPriority++,\n                ),\n            )\n\n            attempts.add(\n                ReadmeAttempt(\n                    path = \"${pathPrefix}README.md\",\n                    filename = \"README.md\",\n                    priority = globalPriority + localPriority++,\n                ),\n            )\n\n            if (primaryLang != \"en\") {\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README.en.md\",\n                        filename = \"README.en.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README.EN.md\",\n                        filename = \"README.EN.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n                attempts.add(\n                    ReadmeAttempt(\n                        path = \"${pathPrefix}README-EN.md\",\n                        filename = \"README-EN.md\",\n                        priority = globalPriority + localPriority++,\n                    ),\n                )\n            }\n\n            globalPriority += 100 * (pathIndex + 1)\n        }\n\n        return attempts.sortedBy { it.priority }\n    }\n\n    fun detectReadmeLanguage(content: String): String? {\n        val sample = content.take(1000)\n        val sampleLower = sample.lowercase()\n\n        val chineseChars = sample.count { it in '\\u4e00'..'\\u9fff' }\n        val japaneseHiragana = sample.count { it in '\\u3040'..'\\u309f' }\n        val japaneseKatakana = sample.count { it in '\\u30a0'..'\\u30ff' }\n        val koreanChars = sample.count { it in '\\uac00'..'\\ud7af' }\n        val arabicChars = sample.count { it in '\\u0600'..'\\u06ff' }\n        val cyrillicChars = sample.count { it in 'а'..'я' || it in 'А'..'Я' || it == 'ё' || it == 'Ё' }\n\n        val totalChars = sample.length\n        val threshold = 0.15\n\n        return when {\n            chineseChars > totalChars * threshold -> {\n                \"zh\"\n            }\n\n            (japaneseHiragana + japaneseKatakana) > totalChars * threshold -> {\n                \"ja\"\n            }\n\n            koreanChars > totalChars * threshold -> {\n                \"ko\"\n            }\n\n            arabicChars > totalChars * threshold -> {\n                \"ar\"\n            }\n\n            cyrillicChars > totalChars * threshold -> {\n                \"ru\"\n            }\n\n            else -> {\n                val englishIndicators =\n                    listOf(\n                        \"\\\\bthe\\\\b\",\n                        \"\\\\band\\\\b\",\n                        \"\\\\bfor\\\\b\",\n                        \"\\\\bwith\\\\b\",\n                        \"\\\\bthis\\\\b\",\n                        \"\\\\bthat\\\\b\",\n                        \"\\\\bfrom\\\\b\",\n                        \"\\\\bare\\\\b\",\n                        \"\\\\bwas\\\\b\",\n                        \"\\\\bhave\\\\b\",\n                        \"\\\\bhas\\\\b\",\n                        \"\\\\bwill\\\\b\",\n                        \"\\\\byou\\\\b\",\n                        \"\\\\bcan\\\\b\",\n                        \"\\\\buse\\\\b\",\n                        \"\\\\binstall\\\\b\",\n                    )\n\n                val matchCount =\n                    englishIndicators.count { pattern ->\n                        Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(sampleLower)\n                    }\n\n                if (matchCount >= 4) {\n                    \"en\"\n                } else {\n                    null\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt",
    "content": "package zed.rainxch.details.data.utils\n\nfun preprocessMarkdown(\n    markdown: String,\n    baseUrl: String,\n): String {\n    val normalizedBaseUrl = if (baseUrl.endsWith(\"/\")) baseUrl else \"$baseUrl/\"\n\n    var processed = markdown\n\n    fun normalizeGitHubUrl(url: String): String =\n        if (url.contains(\"github.com\") && url.contains(\"/blob/\")) {\n            url\n                .replace(\"github.com\", \"raw.githubusercontent.com\")\n                .replace(\"/blob/\", \"/\")\n        } else {\n            url\n        }\n\n    fun isSvgUrl(url: String): Boolean {\n        val lower = url.lowercase()\n        return lower.endsWith(\".svg\") ||\n            lower.contains(\".svg?\") ||\n            lower.contains(\".svg#\") ||\n            lower.contains(\"/svg-badge\") ||\n            lower.contains(\"badge.svg\")\n    }\n\n    fun isBadgeUrl(url: String): Boolean {\n        val lower = url.lowercase()\n        return lower.contains(\"img.shields.io\") ||\n            lower.contains(\"shields.io/badge\") ||\n            lower.contains(\"badge.fury.io\") ||\n            lower.contains(\"badgen.net\") ||\n            lower.contains(\"repology.org/badge\") ||\n            lower.contains(\"hosted.weblate.org/widget\") ||\n            lower.contains(\"codecov.io\") ||\n            lower.contains(\"coveralls.io\") ||\n            lower.contains(\"travis-ci.\") ||\n            lower.contains(\"circleci.com\") ||\n            lower.contains(\"github.com/workflows\") ||\n            (lower.contains(\"/badge\") && isSvgUrl(lower))\n    }\n\n    fun shouldSkipImage(url: String): Boolean = isSvgUrl(url) || isBadgeUrl(url)\n\n    fun resolveUrl(path: String): String {\n        val trimmed = path.trim()\n        val isAbsolute =\n            trimmed.startsWith(\"http://\") ||\n                trimmed.startsWith(\"https://\") ||\n                trimmed.startsWith(\"data:\")\n        return if (isAbsolute) {\n            normalizeGitHubUrl(trimmed)\n        } else {\n            when {\n                trimmed.startsWith(\"./\") -> {\n                    \"$normalizedBaseUrl${trimmed.removePrefix(\"./\")}\"\n                }\n\n                trimmed.startsWith(\"/\") -> {\n                    \"$normalizedBaseUrl${trimmed.removePrefix(\"/\")}\"\n                }\n\n                trimmed.startsWith(\"../\") -> {\n                    var base = normalizedBaseUrl.trimEnd('/')\n                    var rel = trimmed\n                    while (rel.startsWith(\"../\")) {\n                        base = base.substringBeforeLast('/', base)\n                        rel = rel.removePrefix(\"../\")\n                    }\n                    \"$base/$rel\"\n                }\n\n                else -> {\n                    \"$normalizedBaseUrl$trimmed\"\n                }\n            }\n        }\n    }\n\n    // ========================================================================\n    // Phase 0: Handle reference-style markdown definitions and usages\n    // ========================================================================\n    // Reference definitions: [ref-name]: https://example.com/image.svg\n    // Reference usages: ![alt][ref-name] or [![img-ref]][link-ref]\n\n    // 0a. Parse all reference definitions\n    val refDefinitionRegex =\n        Regex(\n            \"\"\"^\\[([^\\]]+)\\]:\\s*(\\S+).*$\"\"\",\n            RegexOption.MULTILINE,\n        )\n    val referenceMap = mutableMapOf<String, String>()\n    for (match in refDefinitionRegex.findAll(processed)) {\n        val refName = match.groupValues[1].lowercase()\n        val url = match.groupValues[2]\n        referenceMap[refName] = url\n    }\n\n    // 0b. Identify which references point to SVGs/badges\n    val skipRefNames =\n        referenceMap\n            .filter { (_, url) ->\n                shouldSkipImage(resolveUrl(url))\n            }.keys\n\n    // 0c. Remove reference-style image usages that point to SVGs: ![alt][svg-ref]\n    if (skipRefNames.isNotEmpty()) {\n        processed =\n            processed.replace(\n                Regex(\"\"\"!\\[([^\\]]*)\\]\\[([^\\]]+)\\]\"\"\"),\n            ) { match ->\n                val alt = match.groupValues[1]\n                val refName = match.groupValues[2].lowercase()\n                if (refName in skipRefNames) {\n                    if (alt.isNotEmpty()) \"**$alt**\" else \"\"\n                } else {\n                    match.value\n                }\n            }\n    }\n\n    // 0d. Resolve remaining reference-style images to inline format: ![alt][ref] → ![alt](url)\n    processed =\n        processed.replace(\n            Regex(\"\"\"!\\[([^\\]]*)\\]\\[([^\\]]+)\\]\"\"\"),\n        ) { match ->\n            val alt = match.groupValues[1]\n            val refName = match.groupValues[2].lowercase()\n            val url = referenceMap[refName]\n            if (url != null) {\n                val resolved = resolveUrl(url)\n                \"![$alt]($resolved)\"\n            } else {\n                match.value\n            }\n        }\n\n    // 0e. Handle nested badge-as-link patterns: [![badge-ref]][link-ref]\n    // After 0c strips the inner image, this can leave [**text**][link-ref] or [][link-ref]\n    processed =\n        processed.replace(\n            Regex(\"\"\"\\[(\\*\\*[^*]*\\*\\*)\\]\\[([^\\]]+)\\]\"\"\"),\n        ) { match ->\n            val boldText = match.groupValues[1]\n            val refName = match.groupValues[2].lowercase()\n            val url = referenceMap[refName]\n            if (url != null) {\n                \"[$boldText](${resolveUrl(url)})\"\n            } else {\n                boldText\n            }\n        }\n    // Clean empty bracket patterns left from stripped badge images: [][ref]\n    processed =\n        processed.replace(\n            Regex(\"\"\"\\[\\s*\\]\\[([^\\]]+)\\]\"\"\"),\n            \"\",\n        )\n\n    // 0f. Handle reference-style links: [text][ref] → [text](url)\n    processed =\n        processed.replace(\n            Regex(\"\"\"\\[([^\\]]+)\\]\\[([^\\]]+)\\]\"\"\"),\n        ) { match ->\n            val text = match.groupValues[1]\n            val refName = match.groupValues[2].lowercase()\n            val url = referenceMap[refName]\n            // Don't convert if text looks like it was already an image (starts with !)\n            if (url != null && !text.startsWith(\"!\")) {\n                \"[$text](${resolveUrl(url)})\"\n            } else {\n                match.value\n            }\n        }\n\n    // 0g. Remove all reference definitions that were resolved\n    processed =\n        processed.replace(\n            Regex(\"\"\"^\\[([^\\]]+)\\]:\\s*\\S+.*$\"\"\", RegexOption.MULTILINE),\n        ) { match ->\n            val refName = match.groupValues[1].lowercase()\n            if (refName in referenceMap) \"\" else match.value\n        }\n\n    // ========================================================================\n    // Phase 1: HTML → Markdown conversions\n    // ========================================================================\n\n    // 1. Unwrap <picture> elements → keep only the <img> fallback\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<picture[^>]*>.*?(<img\\s[^>]*?>).*?</picture>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            match.groupValues[1]\n        }\n    // Also strip orphaned <source> tags (outside <picture>)\n    processed =\n        processed.replace(\n            Regex(\"\"\"<source\\s[^>]*?/?>\"\"\", RegexOption.IGNORE_CASE),\n            \"\",\n        )\n\n    // 2. Unwrap <a> tags that wrap <img> tags — keep the <img> for step 3\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<a\\s[^>]*?>\\s*(<img\\s[^>]*?>)\\s*</a>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            match.groupValues[1]\n        }\n\n    // 3. Convert <img> tags → markdown images (handles multiline img tags)\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<img\\s+([^>]*?)\\s*/?>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { imgMatch ->\n            val imgTag = imgMatch.groupValues[1]\n\n            val srcMatch = Regex(\"\"\"src\\s*=\\s*([\"'])([^\"']+)\\1\"\"\").find(imgTag)\n            val src = srcMatch?.groupValues?.get(2) ?: \"\"\n\n            val altMatch = Regex(\"\"\"alt\\s*=\\s*([\"'])([^\"']*)\\1\"\"\").find(imgTag)\n            val alt = altMatch?.groupValues?.get(2) ?: \"\"\n\n            if (src.isNotEmpty()) {\n                val normalizedSrc = resolveUrl(src)\n\n                if (shouldSkipImage(normalizedSrc)) {\n                    if (alt.isNotEmpty()) \"**$alt**\" else \"\"\n                } else {\n                    \"![$alt]($normalizedSrc)\"\n                }\n            } else {\n                \"\"\n            }\n        }\n\n    // 4. Normalize markdown image URLs (resolve relative, normalize GitHub blob)\n    processed =\n        processed.replace(\n            Regex(\"\"\"!\\[([^\\]]*)\\]\\(([^)]+)\\)\"\"\"),\n        ) { match ->\n            val alt = match.groupValues[1]\n            val originalPath = match.groupValues[2].trim()\n            val finalUrl = resolveUrl(originalPath)\n\n            if (shouldSkipImage(finalUrl)) {\n                if (alt.isNotEmpty()) \"**$alt**\" else \"\"\n            } else {\n                \"![$alt]($finalUrl)\"\n            }\n        }\n\n    // 5. Handle <video> tags → markdown link or remove\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<video[^>]*?\\ssrc=([\"'])([^\"']+)\\1[^>]*>.*?</video>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            val src = match.groupValues[2]\n            \"[Video](${resolveUrl(src)})\"\n        }\n    // Video with <source> inside\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<video[^>]*>.*?<source\\s[^>]*?\\ssrc=([\"'])([^\"']+)\\1[^>]*?>.*?</video>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            val src = match.groupValues[2]\n            \"[Video](${resolveUrl(src)})\"\n        }\n\n    // 6. Convert HTML headings <h1>–<h6> → markdown headings\n    for (level in 1..6) {\n        val hashes = \"#\".repeat(level)\n        processed =\n            processed.replace(\n                Regex(\n                    \"\"\"<h$level[^>]*>(.*?)</h$level>\"\"\",\n                    setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n                ),\n            ) { match ->\n                val content = match.groupValues[1].trim()\n                \"\\n$hashes $content\\n\"\n            }\n    }\n\n    // 7. Convert <br> and <hr> tags\n    processed =\n        processed.replace(\n            Regex(\"\"\"<br\\s*/?>\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\",\n        )\n    processed =\n        processed.replace(\n            Regex(\"\"\"<hr\\s*/?>\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n---\\n\",\n        )\n\n    // 8. Convert inline formatting tags\n    // <b> / <strong> → **text**\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<(b|strong)>(.*?)</\\1>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            \"**${match.groupValues[2]}**\"\n        }\n    // <i> / <em> → *text*\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<(i|em)>(.*?)</\\1>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            \"*${match.groupValues[2]}*\"\n        }\n    // <code> → `text` (single-line only, not <pre><code>)\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<code>([^<]*?)</code>\"\"\",\n                RegexOption.IGNORE_CASE,\n            ),\n        ) { match ->\n            \"`${match.groupValues[1]}`\"\n        }\n    // <s> / <del> / <strike> → ~~text~~\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<(s|del|strike)>(.*?)</\\1>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            \"~~${match.groupValues[2]}~~\"\n        }\n\n    // 9. Convert <a href=\"url\">text</a> → [text](url) (non-image links)\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<a\\s+[^>]*?href\\s*=\\s*([\"'])([^\"']+)\\1[^>]*>(.*?)</a>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            val url = match.groupValues[2]\n            val text = match.groupValues[3].trim()\n            val resolvedUrl = resolveUrl(url)\n            if (text.isEmpty()) {\n                \"[$resolvedUrl]($resolvedUrl)\"\n            } else {\n                \"[$text]($resolvedUrl)\"\n            }\n        }\n\n    // 10. <kbd> → `text`\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<kbd>(.*?)</kbd>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            \"`${match.groupValues[1]}`\"\n        }\n\n    // 11. Strip remaining wrapper tags (keep content)\n    // <div> tags\n    processed =\n        processed.replace(\n            Regex(\"\"\"<div[^>]*?>\\s*\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\\n\",\n        )\n    processed =\n        processed.replace(\n            Regex(\"\"\"</div>\\s*\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\\n\",\n        )\n    // <p> / </p>\n    processed =\n        processed.replace(\n            Regex(\"\"\"<p[^>]*?>\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\",\n        )\n    processed =\n        processed.replace(\n            Regex(\"\"\"</p>\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\",\n        )\n    // <details> / <summary>\n    processed =\n        processed.replace(\n            Regex(\"\"\"<details[^>]*?>\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\",\n        )\n    processed =\n        processed.replace(\n            Regex(\"\"\"</details>\"\"\", RegexOption.IGNORE_CASE),\n            \"\\n\",\n        )\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"<summary[^>]*?>(.*?)</summary>\"\"\",\n                setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL),\n            ),\n        ) { match ->\n            \"**${match.groupValues[1].trim()}**\\n\"\n        }\n    // <span>, <sup>, <sub> — strip tags, keep content\n    processed =\n        processed.replace(\n            Regex(\"\"\"</?(?:span|sup|sub)[^>]*?>\"\"\", RegexOption.IGNORE_CASE),\n            \"\",\n        )\n    // Strip other common straggler HTML tags\n    processed =\n        processed.replace(\n            Regex(\n                \"\"\"</?(?:center|font|u|section|article|header|footer|nav|main|aside|figure|figcaption)[^>]*?>\"\"\",\n                RegexOption.IGNORE_CASE,\n            ),\n            \"\\n\",\n        )\n\n    // 12. Decode common HTML entities\n    processed =\n        processed\n            .replace(\"&amp;\", \"&\")\n            .replace(\"&lt;\", \"<\")\n            .replace(\"&gt;\", \">\")\n            .replace(\"&quot;\", \"\\\"\")\n            .replace(\"&#39;\", \"'\")\n            .replace(\"&apos;\", \"'\")\n            .replace(\"&nbsp;\", \" \")\n    // Numeric HTML entities\n    processed =\n        processed.replace(Regex(\"\"\"&#(\\d+);\"\"\")) { match ->\n            val code = match.groupValues[1].toIntOrNull()\n            if (code != null && code in 32..126) {\n                code.toChar().toString()\n            } else {\n                match.value\n            }\n        }\n\n    // 13. Clean up empty <p> tags and excess newlines\n    processed =\n        processed.replace(\n            Regex(\"\"\"<p[^>]*?>\\s*</p>\"\"\", RegexOption.IGNORE_CASE),\n            \"\",\n        )\n    processed =\n        processed.replace(\n            Regex(\"\"\"\\n{3,}\"\"\"),\n            \"\\n\\n\",\n        )\n\n    // 14. Clean up orphaned markdown link fragments\n    processed =\n        processed.replace(\n            Regex(\"\"\"^\\]\\([^)]+\\)\"\"\", RegexOption.MULTILINE),\n            \"\",\n        )\n\n    return processed.trim()\n}\n"
  },
  {
    "path": "feature/details/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/details/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/ReleaseCategory.kt",
    "content": "package zed.rainxch.details.domain.model\n\nenum class ReleaseCategory {\n    STABLE,\n    PRE_RELEASE,\n    ALL,\n}\n"
  },
  {
    "path": "feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.kt",
    "content": "package zed.rainxch.details.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RepoStats(\n    val stars: Int,\n    val forks: Int,\n    val openIssues: Int,\n)\n"
  },
  {
    "path": "feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SupportedLanguage.kt",
    "content": "package zed.rainxch.details.domain.model\n\ndata class SupportedLanguage(\n    val code: String,\n    val displayName: String,\n)\n"
  },
  {
    "path": "feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/TranslationResult.kt",
    "content": "package zed.rainxch.details.domain.model\n\ndata class TranslationResult(\n    val translatedText: String,\n    val detectedSourceLanguage: String?,\n)\n"
  },
  {
    "path": "feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/repository/DetailsRepository.kt",
    "content": "package zed.rainxch.details.domain.repository\n\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.GithubUserProfile\nimport zed.rainxch.details.domain.model.RepoStats\n\ntypealias ReadmeContent = String\ntypealias ReadmePath = String\ntypealias LanguageCode = String\n\ninterface DetailsRepository {\n    suspend fun getRepositoryById(id: Long): GithubRepoSummary\n\n    suspend fun getRepositoryByOwnerAndName(\n        owner: String,\n        name: String,\n    ): GithubRepoSummary\n\n    suspend fun getLatestPublishedRelease(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): GithubRelease?\n\n    suspend fun getAllReleases(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): List<GithubRelease>\n\n    suspend fun getReadme(\n        owner: String,\n        repo: String,\n        defaultBranch: String,\n    ): Triple<ReadmeContent, LanguageCode?, ReadmePath>?\n\n    suspend fun getRepoStats(\n        owner: String,\n        repo: String,\n    ): RepoStats\n\n    suspend fun getUserProfile(username: String): GithubUserProfile\n\n    suspend fun checkAttestations(\n        owner: String,\n        repo: String,\n        sha256Digest: String,\n    ): Boolean\n}\n"
  },
  {
    "path": "feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/repository/TranslationRepository.kt",
    "content": "package zed.rainxch.details.domain.repository\n\nimport zed.rainxch.details.domain.model.TranslationResult\n\ninterface TranslationRepository {\n    suspend fun translate(\n        text: String,\n        targetLanguage: String,\n        sourceLanguage: String = \"auto\",\n    ): TranslationResult\n\n    fun getDeviceLanguageCode(): String\n}\n"
  },
  {
    "path": "feature/details/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/details/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.details.domain)\n\n                implementation(libs.markdown.renderer)\n                implementation(libs.markdown.renderer.coil3)\n\n                implementation(compose.components.resources)\n                implementation(libs.liquid)\n                implementation(libs.kotlinx.datetime)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(libs.bundles.landscapist)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt",
    "content": "package zed.rainxch.details.presentation\n\nimport org.jetbrains.compose.resources.StringResource\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.details.domain.model.ReleaseCategory\nimport zed.rainxch.details.presentation.model.TranslationTarget\n\nsealed interface DetailsAction {\n    data object Retry : DetailsAction\n\n    data object InstallPrimary : DetailsAction\n\n    data object OnDismissDowngradeWarning : DetailsAction\n\n    data object OnDismissSigningKeyWarning : DetailsAction\n\n    data object OnOverrideSigningKeyWarning : DetailsAction\n\n    data object UninstallApp : DetailsAction\n    data object OnRequestUninstall : DetailsAction\n    data object OnDismissUninstallConfirmation : DetailsAction\n    data object OnConfirmUninstall : DetailsAction\n\n    data class DownloadAsset(\n        val downloadUrl: String,\n        val assetName: String,\n        val sizeBytes: Long,\n    ) : DetailsAction\n\n    data object CancelCurrentDownload : DetailsAction\n\n    data object OpenRepoInBrowser : DetailsAction\n\n    data object OpenAuthorInBrowser : DetailsAction\n\n    data class OpenDeveloperProfile(\n        val username: String,\n    ) : DetailsAction\n\n    data object OpenInObtainium : DetailsAction\n\n    data object OpenInAppManager : DetailsAction\n\n    data object InstallWithExternalApp : DetailsAction\n\n    data object OpenWithExternalInstaller : DetailsAction\n\n    data object DismissExternalInstallerPrompt : DetailsAction\n\n    data object OnToggleInstallDropdown : DetailsAction\n\n    data object OnNavigateBackClick : DetailsAction\n\n    data object OnToggleFavorite : DetailsAction\n\n    data object OnShareClick : DetailsAction\n\n    data object UpdateApp : DetailsAction\n\n    data object OpenApp : DetailsAction\n\n    data class OnMessage(\n        val messageText: StringResource,\n    ) : DetailsAction\n\n    data class SelectReleaseCategory(\n        val category: ReleaseCategory,\n    ) : DetailsAction\n\n    data class SelectRelease(\n        val release: GithubRelease,\n    ) : DetailsAction\n\n    data object ToggleVersionPicker : DetailsAction\n\n    data object ToggleAboutExpanded : DetailsAction\n\n    data object ToggleWhatsNewExpanded : DetailsAction\n\n    data class TranslateAbout(\n        val targetLanguageCode: String,\n    ) : DetailsAction\n\n    data class TranslateWhatsNew(\n        val targetLanguageCode: String,\n    ) : DetailsAction\n\n    data object ToggleAboutTranslation : DetailsAction\n\n    data object ToggleWhatsNewTranslation : DetailsAction\n\n    data class ShowLanguagePicker(\n        val target: TranslationTarget,\n    ) : DetailsAction\n\n    data object DismissLanguagePicker : DetailsAction\n\n    // show release asset picker\n    data class SelectDownloadAsset(\n        val release: GithubAsset,\n    ) : DetailsAction\n\n    data object ToggleReleaseAssetsPicker : DetailsAction\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt",
    "content": "package zed.rainxch.details.presentation\n\nsealed interface DetailsEvent {\n    data class OnOpenRepositoryInApp(\n        val repositoryId: Long,\n    ) : DetailsEvent\n\n    data class OnMessage(\n        val message: String,\n    ) : DetailsEvent\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt",
    "content": "package zed.rainxch.details.presentation\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.CutCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.FavoriteBorder\nimport androidx.compose.material.icons.filled.OpenInBrowser\nimport androidx.compose.material.icons.filled.Share\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material.icons.filled.StarBorder\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport io.github.fletchmckee.liquid.LiquidState\nimport io.github.fletchmckee.liquid.liquefiable\nimport io.github.fletchmckee.liquid.liquid\nimport io.github.fletchmckee.liquid.rememberLiquidState\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.core.presentation.utils.isLiquidFrostAvailable\nimport zed.rainxch.details.presentation.components.LanguagePicker\nimport zed.rainxch.details.presentation.components.sections.about\nimport zed.rainxch.details.presentation.components.sections.author\nimport zed.rainxch.details.presentation.components.sections.header\nimport zed.rainxch.details.presentation.components.sections.logs\nimport zed.rainxch.details.presentation.components.sections.reportIssue\nimport zed.rainxch.details.presentation.components.sections.stats\nimport zed.rainxch.details.presentation.components.sections.whatsNew\nimport zed.rainxch.details.presentation.components.states.ErrorState\nimport zed.rainxch.details.presentation.model.TranslationTarget\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.add_to_favourites\nimport zed.rainxch.githubstore.core.presentation.res.cancel\nimport zed.rainxch.githubstore.core.presentation.res.confirm_uninstall_message\nimport zed.rainxch.githubstore.core.presentation.res.confirm_uninstall_title\nimport zed.rainxch.githubstore.core.presentation.res.dismiss\nimport zed.rainxch.githubstore.core.presentation.res.downgrade_requires_uninstall\nimport zed.rainxch.githubstore.core.presentation.res.downgrade_warning_message\nimport zed.rainxch.githubstore.core.presentation.res.install_anyway\nimport zed.rainxch.githubstore.core.presentation.res.install_permission_blocked_message\nimport zed.rainxch.githubstore.core.presentation.res.install_permission_unavailable\nimport zed.rainxch.githubstore.core.presentation.res.navigate_back\nimport zed.rainxch.githubstore.core.presentation.res.open_repository\nimport zed.rainxch.githubstore.core.presentation.res.open_with_external_installer\nimport zed.rainxch.githubstore.core.presentation.res.remove_from_favourites\nimport zed.rainxch.githubstore.core.presentation.res.repository_not_starred\nimport zed.rainxch.githubstore.core.presentation.res.repository_starred\nimport zed.rainxch.githubstore.core.presentation.res.share_repository\nimport zed.rainxch.githubstore.core.presentation.res.signing_key_changed_message\nimport zed.rainxch.githubstore.core.presentation.res.signing_key_changed_title\nimport zed.rainxch.githubstore.core.presentation.res.star_from_github\nimport zed.rainxch.githubstore.core.presentation.res.uninstall\nimport zed.rainxch.githubstore.core.presentation.res.uninstall_first\nimport zed.rainxch.githubstore.core.presentation.res.unstar_from_github\n\n@Composable\nfun DetailsRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToDeveloperProfile: (username: String) -> Unit,\n    onOpenRepositoryInApp: (repoId: Long) -> Unit,\n    viewModel: DetailsViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    val snackbarHostState = remember { SnackbarHostState() }\n    val coroutineScope = rememberCoroutineScope()\n\n    ObserveAsEvents(viewModel.events) { event ->\n        when (event) {\n            is DetailsEvent.OnOpenRepositoryInApp -> {\n                onOpenRepositoryInApp(event.repositoryId)\n            }\n\n            is DetailsEvent.OnMessage -> {\n                coroutineScope.launch {\n                    snackbarHostState.showSnackbar(event.message)\n                }\n            }\n        }\n    }\n\n    DetailsScreen(\n        state = state,\n        snackbarHostState = snackbarHostState,\n        onAction = { action ->\n            when (action) {\n                DetailsAction.OnNavigateBackClick -> {\n                    onNavigateBack()\n                }\n\n                is DetailsAction.OpenDeveloperProfile -> {\n                    onNavigateToDeveloperProfile(action.username)\n                }\n\n                is DetailsAction.OnMessage -> {\n                    coroutineScope.launch {\n                        snackbarHostState.showSnackbar(getString(action.messageText))\n                    }\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n    )\n\n    state.downgradeWarning?.let { warning ->\n        AlertDialog(\n            onDismissRequest = {\n                viewModel.onAction(DetailsAction.OnDismissDowngradeWarning)\n            },\n            title = {\n                Text(\n                    text = stringResource(Res.string.downgrade_requires_uninstall),\n                )\n            },\n            text = {\n                Text(\n                    text =\n                        stringResource(\n                            Res.string.downgrade_warning_message,\n                            warning.targetVersion,\n                            warning.currentVersion,\n                        ),\n                )\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OnDismissDowngradeWarning)\n                        viewModel.onAction(DetailsAction.UninstallApp)\n                    },\n                ) {\n                    Text(\n                        text = stringResource(Res.string.uninstall_first),\n                        color = MaterialTheme.colorScheme.error,\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OnDismissDowngradeWarning)\n                    },\n                ) {\n                    Text(\n                        text = stringResource(Res.string.cancel),\n                    )\n                }\n            },\n        )\n    }\n\n    // Signing key changed warning dialog\n    state.signingKeyWarning?.let { warning ->\n        AlertDialog(\n            onDismissRequest = {\n                viewModel.onAction(DetailsAction.OnDismissSigningKeyWarning)\n            },\n            title = {\n                Text(\n                    text = stringResource(Res.string.signing_key_changed_title),\n                )\n            },\n            text = {\n                Text(\n                    text =\n                        stringResource(\n                            Res.string.signing_key_changed_message,\n                            warning.expectedFingerprint.take(19),\n                            warning.actualFingerprint.take(19),\n                        ),\n                )\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OnOverrideSigningKeyWarning)\n                    },\n                ) {\n                    Text(\n                        text = stringResource(Res.string.install_anyway),\n                        color = MaterialTheme.colorScheme.error,\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OnDismissSigningKeyWarning)\n                    },\n                ) {\n                    Text(\n                        text = stringResource(Res.string.cancel),\n                    )\n                }\n            },\n        )\n    }\n\n    // Uninstall confirmation dialog\n    if (state.showUninstallConfirmation) {\n        val appName = state.installedApp?.appName ?: \"\"\n        AlertDialog(\n            onDismissRequest = {\n                viewModel.onAction(DetailsAction.OnDismissUninstallConfirmation)\n            },\n            title = {\n                Text(\n                    text = stringResource(Res.string.confirm_uninstall_title),\n                )\n            },\n            text = {\n                Text(\n                    text = stringResource(Res.string.confirm_uninstall_message, appName),\n                )\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OnConfirmUninstall)\n                    },\n                ) {\n                    Text(\n                        text = stringResource(Res.string.uninstall),\n                        color = MaterialTheme.colorScheme.error,\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OnDismissUninstallConfirmation)\n                    },\n                ) {\n                    Text(text = stringResource(Res.string.cancel))\n                }\n            },\n        )\n    }\n\n    if (state.showExternalInstallerPrompt) {\n        AlertDialog(\n            onDismissRequest = {\n                viewModel.onAction(DetailsAction.DismissExternalInstallerPrompt)\n            },\n            title = {\n                Text(text = stringResource(Res.string.install_permission_unavailable))\n            },\n            text = {\n                Text(text = stringResource(Res.string.install_permission_blocked_message))\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.OpenWithExternalInstaller)\n                    },\n                ) {\n                    Text(text = stringResource(Res.string.open_with_external_installer))\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = {\n                        viewModel.onAction(DetailsAction.DismissExternalInstallerPrompt)\n                    },\n                ) {\n                    Text(text = stringResource(Res.string.dismiss))\n                }\n            },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun DetailsScreen(\n    state: DetailsState,\n    onAction: (DetailsAction) -> Unit,\n    snackbarHostState: SnackbarHostState,\n) {\n    val liquidTopbarState = rememberLiquidState()\n\n    CompositionLocalProvider(\n        value = LocalTopbarLiquidState provides liquidTopbarState,\n    ) {\n        Scaffold(\n            topBar = {\n                DetailsTopbar(\n                    state = state,\n                    onAction = onAction,\n                    liquidTopbarState = liquidTopbarState,\n                )\n            },\n            snackbarHost = {\n                SnackbarHost(\n                    hostState = snackbarHostState,\n                )\n            },\n            containerColor = MaterialTheme.colorScheme.background,\n            modifier =\n                Modifier.then(\n                    if (state.isLiquidGlassEnabled) {\n                        Modifier.liquefiable(liquidTopbarState)\n                    } else {\n                        Modifier\n                    },\n                ),\n        ) { innerPadding ->\n\n            LanguagePicker(\n                isVisible = state.isLanguagePickerVisible,\n                selectedLanguageCode =\n                    when (state.languagePickerTarget) {\n                        TranslationTarget.About -> state.aboutTranslation.targetLanguageCode\n                        TranslationTarget.WhatsNew -> state.whatsNewTranslation.targetLanguageCode\n                        null -> null\n                    },\n                deviceLanguageCode = state.deviceLanguageCode,\n                onLanguageSelected = { language ->\n                    when (state.languagePickerTarget) {\n                        TranslationTarget.About -> {\n                            onAction(DetailsAction.TranslateAbout(language.code))\n                        }\n\n                        TranslationTarget.WhatsNew -> {\n                            onAction(\n                                DetailsAction.TranslateWhatsNew(\n                                    language.code,\n                                ),\n                            )\n                        }\n\n                        null -> {}\n                    }\n                    onAction(DetailsAction.DismissLanguagePicker)\n                },\n                onDismiss = { onAction(DetailsAction.DismissLanguagePicker) },\n            )\n\n            if (state.isLoading) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    CircularWavyProgressIndicator()\n                }\n\n                return@Scaffold\n            }\n\n            if (state.errorMessage != null) {\n                ErrorState(state.errorMessage, onAction)\n\n                return@Scaffold\n            }\n\n            BoxWithConstraints(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center,\n            ) {\n                val collapsedSectionHeight = maxHeight * 0.7f\n\n                LazyColumn(\n                    modifier =\n                        Modifier\n                            .fillMaxHeight()\n                            .widthIn(max = 680.dp)\n                            .fillMaxWidth()\n                            .then(\n                                if (state.isLiquidGlassEnabled) {\n                                    Modifier.liquefiable(liquidTopbarState)\n                                } else {\n                                    Modifier\n                                },\n                            ).padding(innerPadding),\n                    contentPadding = PaddingValues(16.dp),\n                    verticalArrangement = Arrangement.spacedBy(24.dp),\n                ) {\n                    header(\n                        state = state,\n                        onAction = onAction,\n                    )\n\n                    state.stats?.let { stats ->\n                        stats(\n                            isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                            repoStats = stats,\n                        )\n                    }\n\n                    if (state.isComingFromUpdate) {\n                        state.selectedRelease?.let { release ->\n                            whatsNew(\n                                release = release,\n                                isExpanded = state.isWhatsNewExpanded,\n                                isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                                onToggleExpanded = { onAction(DetailsAction.ToggleWhatsNewExpanded) },\n                                collapsedHeight = collapsedSectionHeight,\n                                translationState = state.whatsNewTranslation,\n                                onTranslateClick = {\n                                    onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode))\n                                },\n                                onLanguagePickerClick = {\n                                    onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.WhatsNew))\n                                },\n                                onToggleTranslation = {\n                                    onAction(DetailsAction.ToggleWhatsNewTranslation)\n                                },\n                            )\n                        }\n\n                        state.readmeMarkdown?.let {\n                            about(\n                                readmeMarkdown = state.readmeMarkdown,\n                                readmeLanguage = state.readmeLanguage,\n                                isExpanded = state.isAboutExpanded,\n                                isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                                onToggleExpanded = { onAction(DetailsAction.ToggleAboutExpanded) },\n                                collapsedHeight = collapsedSectionHeight,\n                                translationState = state.aboutTranslation,\n                                onTranslateClick = {\n                                    onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode))\n                                },\n                                onLanguagePickerClick = {\n                                    onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.About))\n                                },\n                                onToggleTranslation = {\n                                    onAction(DetailsAction.ToggleAboutTranslation)\n                                },\n                            )\n                        }\n                    } else {\n                        state.readmeMarkdown?.let {\n                            about(\n                                readmeMarkdown = state.readmeMarkdown,\n                                readmeLanguage = state.readmeLanguage,\n                                isExpanded = state.isAboutExpanded,\n                                isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                                onToggleExpanded = { onAction(DetailsAction.ToggleAboutExpanded) },\n                                collapsedHeight = collapsedSectionHeight,\n                                translationState = state.aboutTranslation,\n                                onTranslateClick = {\n                                    onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode))\n                                },\n                                onLanguagePickerClick = {\n                                    onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.About))\n                                },\n                                onToggleTranslation = {\n                                    onAction(DetailsAction.ToggleAboutTranslation)\n                                },\n                            )\n                        }\n\n                        state.selectedRelease?.let { release ->\n                            whatsNew(\n                                release = release,\n                                isExpanded = state.isWhatsNewExpanded,\n                                isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                                onToggleExpanded = { onAction(DetailsAction.ToggleWhatsNewExpanded) },\n                                collapsedHeight = collapsedSectionHeight,\n                                translationState = state.whatsNewTranslation,\n                                onTranslateClick = {\n                                    onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode))\n                                },\n                                onLanguagePickerClick = {\n                                    onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.WhatsNew))\n                                },\n                                onToggleTranslation = {\n                                    onAction(DetailsAction.ToggleWhatsNewTranslation)\n                                },\n                            )\n                        }\n                    }\n\n                    state.repository?.let { repository ->\n                        reportIssue(\n                            repoUrl = repository.htmlUrl,\n                        )\n                    }\n\n                    state.userProfile?.let { userProfile ->\n                        author(\n                            isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                            author = userProfile,\n                            onAction = onAction,\n                        )\n                    }\n\n                    if (state.installLogs.isNotEmpty()) {\n                        logs(state)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun DetailsTopbar(\n    state: DetailsState,\n    onAction: (DetailsAction) -> Unit,\n    liquidTopbarState: LiquidState,\n) {\n    TopAppBar(\n        title = { },\n        navigationIcon = {\n            IconButton(\n                shapes = IconButtonDefaults.shapes(),\n                onClick = {\n                    onAction(DetailsAction.OnNavigateBackClick)\n                },\n            ) {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                    contentDescription = stringResource(Res.string.navigate_back),\n                    modifier = Modifier.size(24.dp),\n                )\n            }\n        },\n        actions = {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                if (state.repository != null) {\n                    IconButton(\n                        onClick = {\n                            onAction(\n                                DetailsAction.OnMessage(\n                                    messageText =\n                                        if (state.isStarred) {\n                                            Res.string.unstar_from_github\n                                        } else {\n                                            Res.string.star_from_github\n                                        },\n                                ),\n                            )\n                        },\n                        shapes = IconButtonDefaults.shapes(),\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector =\n                                if (state.isStarred) {\n                                    Icons.Default.Star\n                                } else {\n                                    Icons.Default.StarBorder\n                                },\n                            contentDescription =\n                                stringResource(\n                                    resource =\n                                        if (state.isStarred) {\n                                            Res.string.repository_starred\n                                        } else {\n                                            Res.string.repository_not_starred\n                                        },\n                                ),\n                        )\n                    }\n\n                    IconButton(\n                        onClick = {\n                            onAction(DetailsAction.OnToggleFavorite)\n                        },\n                        shapes = IconButtonDefaults.shapes(),\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector =\n                                if (state.isFavourite) {\n                                    Icons.Default.Favorite\n                                } else {\n                                    Icons.Default.FavoriteBorder\n                                },\n                            contentDescription =\n                                stringResource(\n                                    resource =\n                                        if (state.isFavourite) {\n                                            Res.string.remove_from_favourites\n                                        } else {\n                                            Res.string.add_to_favourites\n                                        },\n                                ),\n                        )\n                    }\n\n                    IconButton(\n                        onClick = {\n                            onAction(DetailsAction.OnShareClick)\n                        },\n                        shapes = IconButtonDefaults.shapes(),\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Share,\n                            contentDescription = stringResource(Res.string.share_repository),\n                        )\n                    }\n                }\n\n                state.repository?.htmlUrl?.let {\n                    IconButton(\n                        shapes = IconButtonDefaults.shapes(),\n                        onClick = {\n                            onAction(DetailsAction.OpenRepoInBrowser)\n                        },\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.OpenInBrowser,\n                            contentDescription = stringResource(Res.string.open_repository),\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                }\n            }\n        },\n        colors =\n            TopAppBarDefaults.topAppBarColors(\n                containerColor = Color.Transparent,\n            ),\n        modifier =\n            Modifier\n                .shadow(\n                    elevation = 6.dp,\n                    ambientColor = MaterialTheme.colorScheme.surfaceTint,\n                    spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                ).background(\n                    Brush.linearGradient(\n                        0f to MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),\n                        0.5f to MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),\n                        1f to MaterialTheme.colorScheme.surface.copy(alpha = 0.85f),\n                    ),\n                ).then(\n                    if (state.isLiquidGlassEnabled && isLiquidFrostAvailable()) {\n                        Modifier.liquid(liquidTopbarState) {\n                            this.shape = CutCornerShape(0.dp)\n                            this.frost = 5.dp\n                            this.curve = .25f\n                            this.refraction = .05f\n                            this.dispersion = .1f\n                        }\n                    } else {\n                        Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest)\n                    },\n                ),\n    )\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        DetailsScreen(\n            state =\n                DetailsState(\n                    isLoading = false,\n                ),\n            onAction = {},\n            snackbarHostState = SnackbarHostState(),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt",
    "content": "package zed.rainxch.details.presentation\n\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.GithubUserProfile\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.SystemArchitecture\nimport zed.rainxch.details.domain.model.ReleaseCategory\nimport zed.rainxch.details.domain.model.RepoStats\nimport zed.rainxch.details.presentation.model.AttestationStatus\nimport zed.rainxch.details.presentation.model.DowngradeWarning\nimport zed.rainxch.details.presentation.model.DownloadStage\nimport zed.rainxch.details.presentation.model.InstallLogItem\nimport zed.rainxch.details.presentation.model.SigningKeyWarning\nimport zed.rainxch.details.presentation.model.TranslationState\nimport zed.rainxch.details.presentation.model.TranslationTarget\n\ndata class DetailsState(\n    val isLoading: Boolean = true,\n    val errorMessage: String? = null,\n    val userProfile: GithubUserProfile? = null,\n    val repository: GithubRepoSummary? = null,\n    // state for assets\n    val primaryAsset: GithubAsset? = null,\n    val installableAssets: List<GithubAsset> = emptyList(),\n    // state for releases\n    val selectedRelease: GithubRelease? = null,\n    val allReleases: List<GithubRelease> = emptyList(),\n    val isReleaseSelectorVisible: Boolean = false,\n    val selectedReleaseCategory: ReleaseCategory = ReleaseCategory.STABLE,\n    val isVersionPickerVisible: Boolean = false,\n    val stats: RepoStats? = null,\n    val readmeMarkdown: String? = null,\n    val readmeLanguage: String? = null,\n    val installLogs: List<InstallLogItem> = emptyList(),\n    val isDownloading: Boolean = false,\n    val downloadProgressPercent: Int? = null,\n    val downloadedBytes: Long = 0L,\n    val totalBytes: Long? = null,\n    val isInstalling: Boolean = false,\n    val downloadError: String? = null,\n    val installError: String? = null,\n    val downloadStage: DownloadStage = DownloadStage.IDLE,\n    val systemArchitecture: SystemArchitecture = SystemArchitecture.UNKNOWN,\n    val isObtainiumAvailable: Boolean = false,\n    val isObtainiumEnabled: Boolean = false,\n    val isInstallDropdownExpanded: Boolean = false,\n    val isAppManagerAvailable: Boolean = false,\n    val isAppManagerEnabled: Boolean = false,\n    val installedApp: InstalledApp? = null,\n    val isFavourite: Boolean = false,\n    val isStarred: Boolean = false,\n    val isTrackingApp: Boolean = false,\n    val isAboutExpanded: Boolean = false,\n    val isWhatsNewExpanded: Boolean = false,\n    val isLiquidGlassEnabled: Boolean = true,\n    val aboutTranslation: TranslationState = TranslationState(),\n    val whatsNewTranslation: TranslationState = TranslationState(),\n    val isLanguagePickerVisible: Boolean = false,\n    val languagePickerTarget: TranslationTarget? = null,\n    val deviceLanguageCode: String = \"en\",\n    val isComingFromUpdate: Boolean = false,\n    val downgradeWarning: DowngradeWarning? = null,\n    val signingKeyWarning: SigningKeyWarning? = null,\n    val showExternalInstallerPrompt: Boolean = false,\n    val pendingInstallFilePath: String? = null,\n    val showUninstallConfirmation: Boolean = false,\n    val attestationStatus: AttestationStatus = AttestationStatus.UNCHECKED,\n) {\n    val filteredReleases: List<GithubRelease>\n        get() =\n            when (selectedReleaseCategory) {\n                ReleaseCategory.STABLE -> allReleases.filter { !it.isPrerelease }\n                ReleaseCategory.PRE_RELEASE -> allReleases.filter { it.isPrerelease }\n                ReleaseCategory.ALL -> allReleases\n            }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt",
    "content": "package zed.rainxch.details.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.NonCancellable\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.format\nimport kotlinx.datetime.format.char\nimport kotlinx.datetime.toLocalDateTime\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.ApkPackageInfo\nimport zed.rainxch.core.domain.model.FavoriteRepo\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.InstallSource\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.network.Downloader\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.SeenReposRepository\nimport zed.rainxch.core.domain.repository.StarredRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.Installer\nimport zed.rainxch.core.domain.system.PackageMonitor\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\nimport zed.rainxch.core.domain.utils.BrowserHelper\nimport zed.rainxch.core.domain.utils.ShareManager\nimport zed.rainxch.details.domain.model.ReleaseCategory\nimport zed.rainxch.details.domain.repository.DetailsRepository\nimport zed.rainxch.details.domain.repository.TranslationRepository\nimport zed.rainxch.details.presentation.model.AttestationStatus\nimport zed.rainxch.details.presentation.model.DowngradeWarning\nimport zed.rainxch.details.presentation.model.DownloadStage\nimport zed.rainxch.details.presentation.model.InstallLogItem\nimport zed.rainxch.details.presentation.model.LogResult\nimport zed.rainxch.details.presentation.model.LogResult.Error\nimport zed.rainxch.details.presentation.model.SigningKeyWarning\nimport zed.rainxch.details.presentation.model.SupportedLanguages\nimport zed.rainxch.details.presentation.model.TranslationState\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.added_to_favourites\nimport zed.rainxch.githubstore.core.presentation.res.failed_to_open_app\nimport zed.rainxch.githubstore.core.presentation.res.failed_to_share_link\nimport zed.rainxch.githubstore.core.presentation.res.failed_to_uninstall\nimport zed.rainxch.githubstore.core.presentation.res.installer_saved_downloads\nimport zed.rainxch.githubstore.core.presentation.res.link_copied_to_clipboard\nimport zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded\nimport zed.rainxch.githubstore.core.presentation.res.removed_from_favourites\nimport zed.rainxch.githubstore.core.presentation.res.translation_failed\nimport zed.rainxch.githubstore.core.presentation.res.update_package_mismatch\nimport java.io.File\nimport java.io.FileInputStream\nimport java.security.MessageDigest\nimport java.util.concurrent.atomic.AtomicBoolean\nimport kotlin.coroutines.cancellation.CancellationException\nimport kotlin.time.Clock.System\nimport kotlin.time.ExperimentalTime\n\nclass DetailsViewModel(\n    private val repositoryId: Long,\n    private val ownerParam: String,\n    private val repoParam: String,\n    private val detailsRepository: DetailsRepository,\n    private val downloader: Downloader,\n    private val installer: Installer,\n    private val platform: Platform,\n    private val helper: BrowserHelper,\n    private val shareManager: ShareManager,\n    private val installedAppsRepository: InstalledAppsRepository,\n    private val favouritesRepository: FavouritesRepository,\n    private val starredRepository: StarredRepository,\n    private val packageMonitor: PackageMonitor,\n    private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase,\n    private val translationRepository: TranslationRepository,\n    private val logger: GitHubStoreLogger,\n    private val isComingFromUpdate: Boolean,\n    private val tweaksRepository: TweaksRepository,\n    private val seenReposRepository: SeenReposRepository,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n    private var currentDownloadJob: Job? = null\n    private var currentAssetName: String? = null\n    private var aboutTranslationJob: Job? = null\n    private var whatsNewTranslationJob: Job? = null\n\n    private var cachedDownloadAssetName: String? = null\n\n    private val _state = MutableStateFlow(DetailsState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    loadInitial()\n                    observeLiquidGlassEnabled()\n\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                viewModelScope,\n                SharingStarted.WhileSubscribed(5000),\n                DetailsState(),\n            )\n\n    private val _events = Channel<DetailsEvent>()\n    val events = _events.receiveAsFlow()\n\n    private val rateLimited = AtomicBoolean(false)\n\n    private fun observeLiquidGlassEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getLiquidGlassEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(isLiquidGlassEnabled = enabled)\n                }\n            }\n        }\n    }\n\n    private fun recomputeAssetsForRelease(release: GithubRelease?): Pair<List<GithubAsset>, GithubAsset?> {\n        val installable =\n            release\n                ?.assets\n                ?.filter { asset ->\n                    installer.isAssetInstallable(asset.name)\n                }.orEmpty()\n        val primary = installer.choosePrimaryAsset(installable)\n        return installable to primary\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private fun loadInitial() {\n        viewModelScope.launch {\n            try {\n                rateLimited.set(false)\n\n                _state.value = _state.value.copy(isLoading = true, errorMessage = null)\n\n                val syncResult = syncInstalledAppsUseCase()\n                if (syncResult.isFailure) {\n                    logger.warn(\"Sync had issues but continuing: ${syncResult.exceptionOrNull()?.message}\")\n                }\n\n                val repo =\n                    if (ownerParam.isNotEmpty() && repoParam.isNotEmpty()) {\n                        detailsRepository.getRepositoryByOwnerAndName(ownerParam, repoParam)\n                    } else {\n                        detailsRepository.getRepositoryById(repositoryId)\n                    }\n                launch { seenReposRepository.markAsSeen(repo.id) }\n\n                val isFavoriteDeferred =\n                    async {\n                        try {\n                            favouritesRepository.isFavoriteSync(repo.id)\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            null\n                        } catch (t: Throwable) {\n                            logger.error(\"Failed to load if repo is favourite: ${t.localizedMessage}\")\n                            false\n                        }\n                    }\n                val isFavorite = isFavoriteDeferred.await()\n                val isStarredDeferred =\n                    async {\n                        try {\n                            starredRepository.isStarred(repo.id)\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            null\n                        } catch (t: Throwable) {\n                            logger.error(\"Failed to load if repo is starred: ${t.localizedMessage}\")\n                            false\n                        }\n                    }\n                val isStarred = isStarredDeferred.await()\n\n                val owner = repo.owner.login\n                val name = repo.name\n\n                _state.value =\n                    _state.value.copy(\n                        repository = repo,\n                        isFavourite = isFavorite == true,\n                        isStarred = isStarred == true,\n                    )\n\n                val allReleasesDeferred =\n                    async {\n                        try {\n                            detailsRepository.getAllReleases(\n                                owner = owner,\n                                repo = name,\n                                defaultBranch = repo.defaultBranch,\n                            )\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            emptyList()\n                        } catch (t: Throwable) {\n                            logger.warn(\"Failed to load releases: ${t.message}\")\n                            emptyList()\n                        }\n                    }\n\n                val statsDeferred =\n                    async {\n                        try {\n                            detailsRepository.getRepoStats(owner, name)\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            null\n                        } catch (_: Throwable) {\n                            null\n                        }\n                    }\n\n                val readmeDeferred =\n                    async {\n                        try {\n                            detailsRepository.getReadme(\n                                owner = owner,\n                                repo = name,\n                                defaultBranch = repo.defaultBranch,\n                            )\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            null\n                        } catch (_: Throwable) {\n                            null\n                        }\n                    }\n\n                val userProfileDeferred =\n                    async {\n                        try {\n                            detailsRepository.getUserProfile(owner)\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            null\n                        } catch (t: Throwable) {\n                            logger.warn(\"Failed to load user profile: ${t.message}\")\n                            null\n                        }\n                    }\n\n                val installedAppDeferred =\n                    async {\n                        try {\n                            val dbApp = installedAppsRepository.getAppByRepoId(repo.id)\n\n                            if (dbApp != null) {\n                                if (dbApp.isPendingInstall &&\n                                    packageMonitor.isPackageInstalled(dbApp.packageName)\n                                ) {\n                                    installedAppsRepository.updatePendingStatus(\n                                        dbApp.packageName,\n                                        false,\n                                    )\n                                    installedAppsRepository.getAppByPackage(dbApp.packageName)\n                                } else {\n                                    dbApp\n                                }\n                            } else {\n                                null\n                            }\n                        } catch (_: RateLimitException) {\n                            rateLimited.set(true)\n                            null\n                        } catch (t: Throwable) {\n                            logger.error(\"Failed to load installed app: ${t.message}\")\n                            null\n                        }\n                    }\n\n                val isObtainiumEnabled = platform == Platform.ANDROID\n                val isAppManagerEnabled = platform == Platform.ANDROID\n\n                val allReleases = allReleasesDeferred.await()\n                val stats = statsDeferred.await()\n                val readme = readmeDeferred.await()\n                val userProfile = userProfileDeferred.await()\n                val installedApp = installedAppDeferred.await()\n\n                if (rateLimited.get()) {\n                    _state.value = _state.value.copy(isLoading = false, errorMessage = null)\n                    return@launch\n                }\n\n                val selectedRelease =\n                    allReleases.firstOrNull { !it.isPrerelease }\n                        ?: allReleases.firstOrNull()\n\n                val (installable, primary) = recomputeAssetsForRelease(selectedRelease)\n\n                val isObtainiumAvailable = installer.isObtainiumInstalled()\n                val isAppManagerAvailable = installer.isAppManagerInstalled()\n\n                logger.debug(\"Loaded repo: ${repo.name}, installedApp: ${installedApp?.packageName}\")\n\n                _state.value =\n                    _state.value.copy(\n                        isLoading = false,\n                        errorMessage = null,\n                        repository = repo,\n                        allReleases = allReleases,\n                        selectedRelease = selectedRelease,\n                        selectedReleaseCategory = ReleaseCategory.STABLE,\n                        stats = stats,\n                        readmeMarkdown = readme?.first,\n                        readmeLanguage = readme?.second,\n                        installableAssets = installable,\n                        primaryAsset = primary,\n                        userProfile = userProfile,\n                        systemArchitecture = installer.detectSystemArchitecture(),\n                        isObtainiumAvailable = isObtainiumAvailable,\n                        isObtainiumEnabled = isObtainiumEnabled,\n                        isAppManagerAvailable = isAppManagerAvailable,\n                        isAppManagerEnabled = isAppManagerEnabled,\n                        installedApp = installedApp,\n                        deviceLanguageCode = translationRepository.getDeviceLanguageCode(),\n                        isComingFromUpdate = isComingFromUpdate,\n                    )\n\n                observeInstalledApp(repo.id)\n            } catch (e: RateLimitException) {\n                logger.error(\"Rate limited: ${e.message}\")\n                _state.value =\n                    _state.value.copy(\n                        isLoading = false,\n                        errorMessage = getString(Res.string.rate_limit_exceeded),\n                    )\n            } catch (t: Throwable) {\n                logger.error(\"Details load failed: ${t.message}\")\n                _state.value =\n                    _state.value.copy(\n                        isLoading = false,\n                        errorMessage = t.message ?: \"Failed to load details\",\n                    )\n            }\n        }\n    }\n\n    private fun observeInstalledApp(repoId: Long) {\n        viewModelScope.launch {\n            installedAppsRepository\n                .getAppByRepoIdAsFlow(repoId)\n                .distinctUntilChanged()\n                .collect { app ->\n                    _state.update { it.copy(installedApp = app) }\n                }\n        }\n    }\n\n    @OptIn(ExperimentalTime::class)\n    fun onAction(action: DetailsAction) {\n        when (action) {\n            DetailsAction.Retry -> {\n                hasLoadedInitialData = false\n                loadInitial()\n            }\n\n            DetailsAction.OnDismissDowngradeWarning -> {\n                _state.update {\n                    it.copy(\n                        downgradeWarning = null,\n                    )\n                }\n            }\n\n            DetailsAction.OnDismissSigningKeyWarning -> {\n                _state.update {\n                    it.copy(\n                        signingKeyWarning = null,\n                        downloadStage = DownloadStage.IDLE,\n                    )\n                }\n                currentAssetName = null\n            }\n\n            DetailsAction.OnOverrideSigningKeyWarning -> {\n                val warning = _state.value.signingKeyWarning ?: return\n                _state.update { it.copy(signingKeyWarning = null) }\n                viewModelScope.launch {\n                    try {\n                        val ext = warning.pendingAssetName.substringAfterLast('.', \"\").lowercase()\n                        installer.install(warning.pendingFilePath, ext)\n\n                        if (platform == Platform.ANDROID) {\n                            saveInstalledAppToDatabase(\n                                assetName = warning.pendingAssetName,\n                                assetUrl = warning.pendingDownloadUrl,\n                                assetSize = warning.pendingSizeBytes,\n                                releaseTag = warning.pendingReleaseTag,\n                                isUpdate = warning.pendingIsUpdate,\n                                filePath = warning.pendingFilePath,\n                            )\n                        }\n\n                        _state.value = _state.value.copy(downloadStage = DownloadStage.IDLE)\n                        currentAssetName = null\n                        appendLog(\n                            assetName = warning.pendingAssetName,\n                            size = warning.pendingSizeBytes,\n                            tag = warning.pendingReleaseTag,\n                            result = if (warning.pendingIsUpdate) LogResult.Updated else LogResult.Installed,\n                        )\n                    } catch (t: Throwable) {\n                        logger.error(\"Install after override failed: ${t.message}\")\n                        _state.value =\n                            _state.value.copy(\n                                downloadStage = DownloadStage.IDLE,\n                                installError = t.message,\n                            )\n                        currentAssetName = null\n                    }\n                }\n            }\n\n            DetailsAction.InstallPrimary -> {\n                val primary = _state.value.primaryAsset\n                val release = _state.value.selectedRelease\n                val installedApp = _state.value.installedApp\n\n                if (primary != null && release != null) {\n                    if (installedApp != null &&\n                        !installedApp.isPendingInstall &&\n                        normalizeVersion(release.tagName) != normalizeVersion(installedApp.installedVersion) &&\n                        platform == Platform.ANDROID\n                    ) {\n                        val isDowngrade =\n                            isDowngradeVersion(\n                                candidate = release.tagName,\n                                current = installedApp.installedVersion,\n                                allReleases = _state.value.allReleases,\n                            )\n\n                        if (isDowngrade) {\n                            _state.update {\n                                it.copy(\n                                    downgradeWarning =\n                                        DowngradeWarning(\n                                            packageName = installedApp.packageName,\n                                            currentVersion = installedApp.installedVersion,\n                                            targetVersion = release.tagName,\n                                        ),\n                                )\n                            }\n                            return\n                        }\n                    }\n\n                    installAsset(\n                        downloadUrl = primary.downloadUrl,\n                        assetName = primary.name,\n                        sizeBytes = primary.size,\n                        releaseTag = release.tagName,\n                    )\n                }\n            }\n\n            DetailsAction.OnRequestUninstall -> {\n                _state.update { it.copy(showUninstallConfirmation = true) }\n            }\n\n            DetailsAction.OnDismissUninstallConfirmation -> {\n                _state.update { it.copy(showUninstallConfirmation = false) }\n            }\n\n            DetailsAction.OnConfirmUninstall -> {\n                _state.update { it.copy(showUninstallConfirmation = false) }\n                val installedApp = _state.value.installedApp ?: return\n                logger.debug(\"Uninstalling app (confirmed): ${installedApp.packageName}\")\n                viewModelScope.launch {\n                    try {\n                        installer.uninstall(installedApp.packageName)\n                    } catch (e: Exception) {\n                        logger.error(\"Failed to request uninstall for ${installedApp.packageName}: ${e.message}\")\n                        _events.send(\n                            DetailsEvent.OnMessage(\n                                getString(Res.string.failed_to_uninstall, installedApp.packageName),\n                            ),\n                        )\n                    }\n                }\n            }\n\n            DetailsAction.UninstallApp -> {\n                // Legacy direct uninstall (used from downgrade warning flow)\n                val installedApp = _state.value.installedApp ?: return\n                logger.debug(\"Uninstalling app: ${installedApp.packageName}\")\n                viewModelScope.launch {\n                    try {\n                        installer.uninstall(installedApp.packageName)\n                    } catch (e: Exception) {\n                        logger.error(\"Failed to request uninstall for ${installedApp.packageName}: ${e.message}\")\n                        _events.send(\n                            DetailsEvent.OnMessage(\n                                getString(Res.string.failed_to_uninstall, installedApp.packageName),\n                            ),\n                        )\n                    }\n                }\n            }\n\n            is DetailsAction.DownloadAsset -> {\n                val release = _state.value.selectedRelease\n                downloadAsset(\n                    downloadUrl = action.downloadUrl,\n                    assetName = action.assetName,\n                    sizeBytes = action.sizeBytes,\n                    releaseTag = release?.tagName ?: \"\",\n                )\n            }\n\n            DetailsAction.CancelCurrentDownload -> {\n                currentDownloadJob?.cancel()\n                currentDownloadJob = null\n\n                val assetName = currentAssetName\n                if (assetName != null) {\n                    cachedDownloadAssetName = assetName\n                    val releaseTag = _state.value.selectedRelease?.tagName ?: \"\"\n                    val totalSize = _state.value.totalBytes ?: _state.value.downloadedBytes\n                    appendLog(\n                        assetName = assetName,\n                        tag = releaseTag,\n                        size = totalSize,\n                        result = LogResult.Cancelled,\n                    )\n                    logger.debug(\"Download cancelled – keeping file for potential reuse: $assetName\")\n                }\n\n                currentAssetName = null\n                _state.value =\n                    _state.value.copy(\n                        isDownloading = false,\n                        downloadProgressPercent = null,\n                        downloadStage = DownloadStage.IDLE,\n                    )\n            }\n\n            DetailsAction.OnToggleFavorite -> {\n                viewModelScope.launch {\n                    try {\n                        val repo = _state.value.repository ?: return@launch\n                        val selectedRelease = _state.value.selectedRelease\n\n                        val favoriteRepo =\n                            FavoriteRepo(\n                                repoId = repo.id,\n                                repoName = repo.name,\n                                repoOwner = repo.owner.login,\n                                repoOwnerAvatarUrl = repo.owner.avatarUrl,\n                                repoDescription = repo.description,\n                                primaryLanguage = repo.language,\n                                repoUrl = repo.htmlUrl,\n                                latestVersion = selectedRelease?.tagName,\n                                latestReleaseUrl = selectedRelease?.htmlUrl,\n                                addedAt = System.now().toEpochMilliseconds(),\n                                lastSyncedAt = System.now().toEpochMilliseconds(),\n                            )\n\n                        favouritesRepository.toggleFavorite(favoriteRepo)\n\n                        val newFavoriteState = favouritesRepository.isFavoriteSync(repo.id)\n                        _state.value = _state.value.copy(isFavourite = newFavoriteState)\n\n                        _events.send(\n                            element =\n                                DetailsEvent.OnMessage(\n                                    message =\n                                        getString(\n                                            resource =\n                                                if (newFavoriteState) {\n                                                    Res.string.added_to_favourites\n                                                } else {\n                                                    Res.string.removed_from_favourites\n                                                },\n                                        ),\n                                ),\n                        )\n                    } catch (t: Throwable) {\n                        logger.error(\"Failed to toggle favorite: ${t.message}\")\n                    }\n                }\n            }\n\n            DetailsAction.OnShareClick -> {\n                viewModelScope.launch {\n                    _state.value.repository?.let { repo ->\n                        runCatching {\n                            shareManager.shareText(\"https://github-store.org/app?repo=${repo.fullName}\")\n                        }.onFailure { t ->\n                            logger.error(\"Failed to share link: ${t.message}\")\n                            _events.send(\n                                DetailsEvent.OnMessage(getString(Res.string.failed_to_share_link)),\n                            )\n                            return@launch\n                        }\n\n                        if (platform != Platform.ANDROID) {\n                            _events.send(DetailsEvent.OnMessage(getString(Res.string.link_copied_to_clipboard)))\n                        }\n                    }\n                }\n            }\n\n            DetailsAction.UpdateApp -> {\n                val installedApp = _state.value.installedApp\n                val selectedRelease = _state.value.selectedRelease\n\n                if (installedApp != null && selectedRelease != null && installedApp.isUpdateAvailable) {\n                    val latestAsset =\n                        _state.value.installableAssets.firstOrNull {\n                            it.name == installedApp.latestAssetName\n                        } ?: _state.value.primaryAsset\n\n                    if (latestAsset != null) {\n                        installAsset(\n                            downloadUrl = latestAsset.downloadUrl,\n                            assetName = latestAsset.name,\n                            sizeBytes = latestAsset.size,\n                            releaseTag = selectedRelease.tagName,\n                            isUpdate = true,\n                        )\n                    }\n                }\n            }\n\n            DetailsAction.OpenApp -> {\n                val installedApp = _state.value.installedApp ?: return\n                val launched = installer.openApp(installedApp.packageName)\n                if (!launched) {\n                    viewModelScope.launch {\n                        _events.send(\n                            DetailsEvent.OnMessage(\n                                getString(\n                                    Res.string.failed_to_open_app,\n                                    installedApp.appName,\n                                ),\n                            ),\n                        )\n                    }\n                }\n            }\n\n            DetailsAction.OpenRepoInBrowser -> {\n                _state.value.repository?.htmlUrl?.let {\n                    helper.openUrl(url = it)\n                }\n            }\n\n            DetailsAction.OpenAuthorInBrowser -> {\n                _state.value.userProfile?.htmlUrl?.let {\n                    helper.openUrl(url = it)\n                }\n            }\n\n            DetailsAction.OpenInObtainium -> {\n                val repo = _state.value.repository\n                repo?.owner?.login?.let {\n                    installer.openInObtainium(\n                        repoOwner = it,\n                        repoName = repo.name,\n                        onOpenInstaller = {\n                            viewModelScope.launch {\n                                _events.send(\n                                    DetailsEvent.OnOpenRepositoryInApp(OBTAINIUM_REPO_ID),\n                                )\n                            }\n                        },\n                    )\n                }\n                _state.update {\n                    it.copy(isInstallDropdownExpanded = false)\n                }\n            }\n\n            DetailsAction.OpenInAppManager -> {\n                viewModelScope.launch {\n                    try {\n                        val primary = _state.value.primaryAsset\n                        val release = _state.value.selectedRelease\n\n                        if (primary != null && release != null) {\n                            currentAssetName = primary.name\n\n                            appendLog(\n                                assetName = primary.name,\n                                size = primary.size,\n                                tag = release.tagName,\n                                result = LogResult.PreparingForAppManager,\n                            )\n\n                            _state.value =\n                                _state.value.copy(\n                                    downloadError = null,\n                                    installError = null,\n                                    downloadProgressPercent = null,\n                                    downloadStage = DownloadStage.DOWNLOADING,\n                                )\n\n                            downloader.download(primary.downloadUrl, primary.name).collect { p ->\n                                _state.value =\n                                    _state.value.copy(downloadProgressPercent = p.percent)\n                                if (p.percent == 100) {\n                                    _state.value =\n                                        _state.value.copy(downloadStage = DownloadStage.VERIFYING)\n                                }\n                            }\n\n                            val filePath =\n                                downloader.getDownloadedFilePath(primary.name)\n                                    ?: throw IllegalStateException(\"Downloaded file not found\")\n\n                            appendLog(\n                                assetName = primary.name,\n                                size = primary.size,\n                                tag = release.tagName,\n                                result = LogResult.Downloaded,\n                            )\n\n                            _state.value = _state.value.copy(downloadStage = DownloadStage.IDLE)\n                            currentAssetName = null\n\n                            installer.openInAppManager(\n                                filePath = filePath,\n                                onOpenInstaller = {\n                                    viewModelScope.launch {\n                                        _events.send(\n                                            DetailsEvent.OnOpenRepositoryInApp(APP_MANAGER_REPO_ID),\n                                        )\n                                    }\n                                },\n                            )\n\n                            appendLog(\n                                assetName = primary.name,\n                                size = primary.size,\n                                tag = release.tagName,\n                                result = LogResult.OpenedInAppManager,\n                            )\n                        }\n                    } catch (t: Throwable) {\n                        logger.error(\"Failed to open in AppManager: ${t.message}\")\n                        _state.value =\n                            _state.value.copy(\n                                downloadStage = DownloadStage.IDLE,\n                                installError = t.message,\n                            )\n                        currentAssetName = null\n\n                        _state.value.primaryAsset?.let { asset ->\n                            _state.value.selectedRelease?.let { release ->\n                                appendLog(\n                                    assetName = asset.name,\n                                    size = asset.size,\n                                    tag = release.tagName,\n                                    result = LogResult.Error(t.message),\n                                )\n                            }\n                        }\n                    }\n                }\n                _state.update {\n                    it.copy(isInstallDropdownExpanded = false)\n                }\n            }\n\n            DetailsAction.OnToggleInstallDropdown -> {\n                _state.update {\n                    it.copy(isInstallDropdownExpanded = !it.isInstallDropdownExpanded)\n                }\n            }\n\n            is DetailsAction.SelectReleaseCategory -> {\n                val newCategory = action.category\n                val filtered =\n                    when (newCategory) {\n                        ReleaseCategory.STABLE -> _state.value.allReleases.filter { !it.isPrerelease }\n                        ReleaseCategory.PRE_RELEASE -> _state.value.allReleases.filter { it.isPrerelease }\n                        ReleaseCategory.ALL -> _state.value.allReleases\n                    }\n                val newSelected = filtered.firstOrNull()\n                val (installable, primary) = recomputeAssetsForRelease(newSelected)\n\n                whatsNewTranslationJob?.cancel()\n                _state.update {\n                    it.copy(\n                        selectedReleaseCategory = newCategory,\n                        selectedRelease = newSelected,\n                        installableAssets = installable,\n                        primaryAsset = primary,\n                        whatsNewTranslation = TranslationState(),\n                    )\n                }\n            }\n\n            is DetailsAction.SelectRelease -> {\n                val release = action.release\n                val (installable, primary) = recomputeAssetsForRelease(release)\n                whatsNewTranslationJob?.cancel()\n\n                _state.update {\n                    it.copy(\n                        selectedRelease = release,\n                        installableAssets = installable,\n                        primaryAsset = primary,\n                        isVersionPickerVisible = false,\n                        whatsNewTranslation = TranslationState(),\n                    )\n                }\n            }\n\n            DetailsAction.ToggleVersionPicker -> {\n                _state.update {\n                    it.copy(isVersionPickerVisible = !it.isVersionPickerVisible)\n                }\n            }\n\n            DetailsAction.ToggleAboutExpanded -> {\n                _state.update {\n                    it.copy(isAboutExpanded = !it.isAboutExpanded)\n                }\n            }\n\n            DetailsAction.ToggleWhatsNewExpanded -> {\n                _state.update {\n                    it.copy(isWhatsNewExpanded = !it.isWhatsNewExpanded)\n                }\n            }\n\n            is DetailsAction.TranslateAbout -> {\n                val readme = _state.value.readmeMarkdown ?: return\n                aboutTranslationJob?.cancel()\n                aboutTranslationJob =\n                    translateContent(\n                        text = readme,\n                        targetLanguageCode = action.targetLanguageCode,\n                        updateState = { ts -> _state.update { it.copy(aboutTranslation = ts) } },\n                        getCurrentState = { _state.value.aboutTranslation },\n                    )\n            }\n\n            is DetailsAction.TranslateWhatsNew -> {\n                val description = _state.value.selectedRelease?.description ?: return\n                whatsNewTranslationJob?.cancel()\n                whatsNewTranslationJob =\n                    translateContent(\n                        text = description,\n                        targetLanguageCode = action.targetLanguageCode,\n                        updateState = { ts -> _state.update { it.copy(whatsNewTranslation = ts) } },\n                        getCurrentState = { _state.value.whatsNewTranslation },\n                    )\n            }\n\n            DetailsAction.ToggleAboutTranslation -> {\n                _state.update {\n                    val current = it.aboutTranslation\n                    it.copy(aboutTranslation = current.copy(isShowingTranslation = !current.isShowingTranslation))\n                }\n            }\n\n            DetailsAction.ToggleWhatsNewTranslation -> {\n                _state.update {\n                    val current = it.whatsNewTranslation\n                    it.copy(whatsNewTranslation = current.copy(isShowingTranslation = !current.isShowingTranslation))\n                }\n            }\n\n            is DetailsAction.ShowLanguagePicker -> {\n                _state.update {\n                    it.copy(\n                        isLanguagePickerVisible = true,\n                        languagePickerTarget = action.target,\n                    )\n                }\n            }\n\n            DetailsAction.DismissLanguagePicker -> {\n                _state.update {\n                    it.copy(isLanguagePickerVisible = false, languagePickerTarget = null)\n                }\n            }\n\n            DetailsAction.OpenWithExternalInstaller -> {\n                val filePath = _state.value.pendingInstallFilePath\n                if (filePath != null) {\n                    try {\n                        installer.openWithExternalInstaller(filePath)\n                        _state.value.primaryAsset?.let { asset ->\n                            _state.value.selectedRelease?.let { release ->\n                                appendLog(\n                                    assetName = asset.name,\n                                    size = asset.size,\n                                    tag = release.tagName,\n                                    result = LogResult.OpenedInExternalInstaller,\n                                )\n                            }\n                        }\n                    } catch (t: Throwable) {\n                        logger.error(\"Failed to open with external installer: ${t.message}\")\n                        _state.value = _state.value.copy(installError = t.message)\n                    }\n                }\n                _state.value =\n                    _state.value.copy(\n                        showExternalInstallerPrompt = false,\n                        pendingInstallFilePath = null,\n                    )\n            }\n\n            DetailsAction.DismissExternalInstallerPrompt -> {\n                _state.value =\n                    _state.value.copy(\n                        showExternalInstallerPrompt = false,\n                        pendingInstallFilePath = null,\n                    )\n            }\n\n            DetailsAction.InstallWithExternalApp -> {\n                currentDownloadJob?.cancel()\n                val job =\n                    viewModelScope.launch {\n                        try {\n                            val primary = _state.value.primaryAsset\n                            val release = _state.value.selectedRelease\n\n                            if (primary != null && release != null) {\n                                currentAssetName = primary.name\n\n                                appendLog(\n                                    assetName = primary.name,\n                                    size = primary.size,\n                                    tag = release.tagName,\n                                    result = LogResult.DownloadStarted,\n                                )\n\n                                _state.value =\n                                    _state.value.copy(\n                                        downloadError = null,\n                                        installError = null,\n                                        downloadProgressPercent = null,\n                                        downloadStage = DownloadStage.DOWNLOADING,\n                                    )\n\n                                downloader\n                                    .download(primary.downloadUrl, primary.name)\n                                    .collect { p ->\n                                        _state.value =\n                                            _state.value.copy(downloadProgressPercent = p.percent)\n                                        if (p.percent == 100) {\n                                            _state.value =\n                                                _state.value.copy(downloadStage = DownloadStage.VERIFYING)\n                                        }\n                                    }\n\n                                val filePath =\n                                    downloader.getDownloadedFilePath(primary.name)\n                                        ?: throw IllegalStateException(\"Downloaded file not found\")\n\n                                appendLog(\n                                    assetName = primary.name,\n                                    size = primary.size,\n                                    tag = release.tagName,\n                                    result = LogResult.Downloaded,\n                                )\n\n                                _state.value = _state.value.copy(downloadStage = DownloadStage.IDLE)\n                                currentAssetName = null\n\n                                installer.openWithExternalInstaller(filePath)\n\n                                appendLog(\n                                    assetName = primary.name,\n                                    size = primary.size,\n                                    tag = release.tagName,\n                                    result = LogResult.OpenedInExternalInstaller,\n                                )\n                            }\n                        } catch (e: CancellationException) {\n                            logger.debug(\"Install with external app cancelled\")\n                            _state.value = _state.value.copy(downloadStage = DownloadStage.IDLE)\n                            currentAssetName = null\n                            throw e\n                        } catch (t: Throwable) {\n                            logger.error(\"Failed to install with external app: ${t.message}\")\n                            _state.value =\n                                _state.value.copy(\n                                    downloadStage = DownloadStage.IDLE,\n                                    installError = t.message,\n                                )\n                            currentAssetName = null\n\n                            _state.value.primaryAsset?.let { asset ->\n                                _state.value.selectedRelease?.let { release ->\n                                    appendLog(\n                                        assetName = asset.name,\n                                        size = asset.size,\n                                        tag = release.tagName,\n                                        result = Error(t.message),\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                currentDownloadJob = job\n                job.invokeOnCompletion {\n                    if (currentDownloadJob === job) {\n                        currentDownloadJob = null\n                    }\n                }\n\n                _state.update {\n                    it.copy(isInstallDropdownExpanded = false)\n                }\n            }\n\n            DetailsAction.OnNavigateBackClick -> {\n                // Handled in composable\n            }\n\n            is DetailsAction.OpenDeveloperProfile -> {\n                // Handled in composable\n            }\n\n            is DetailsAction.OnMessage -> {\n                // Handled in composable\n            }\n\n            is DetailsAction.SelectDownloadAsset -> {\n                _state.update { state -> state.copy(primaryAsset = action.release) }\n            }\n\n            DetailsAction.ToggleReleaseAssetsPicker -> {\n                _state.update { state -> state.copy(isReleaseSelectorVisible = !state.isReleaseSelectorVisible) }\n            }\n        }\n    }\n\n    private fun installAsset(\n        downloadUrl: String,\n        assetName: String,\n        sizeBytes: Long,\n        releaseTag: String,\n        isUpdate: Boolean = false,\n    ) {\n        currentDownloadJob?.cancel()\n        currentDownloadJob =\n            viewModelScope.launch {\n                try {\n                    val filePath: String =\n                        downloadAsset(\n                            assetName = assetName,\n                            sizeBytes = sizeBytes,\n                            releaseTag = releaseTag,\n                            isUpdate = isUpdate,\n                            downloadUrl = downloadUrl,\n                        ) ?: return@launch\n\n                    installAsset(\n                        isUpdate = isUpdate,\n                        filePath = filePath,\n                        assetName = assetName,\n                        downloadUrl = downloadUrl,\n                        sizeBytes = sizeBytes,\n                        releaseTag = releaseTag,\n                    )\n                } catch (e: kotlinx.coroutines.CancellationException) {\n                    throw e\n                } catch (t: Throwable) {\n                    logger.error(\"Install failed: ${t.message}\")\n                    t.printStackTrace()\n                    _state.value =\n                        _state.value.copy(\n                            downloadStage = DownloadStage.IDLE,\n                            installError = t.message,\n                        )\n                    currentAssetName = null\n                    appendLog(\n                        assetName = assetName,\n                        size = sizeBytes,\n                        tag = releaseTag,\n                        result = Error(t.message),\n                    )\n                }\n            }\n    }\n\n    private suspend fun installAsset(\n        isUpdate: Boolean,\n        filePath: String,\n        assetName: String,\n        downloadUrl: String,\n        sizeBytes: Long,\n        releaseTag: String,\n    ) {\n        _state.value = _state.value.copy(downloadStage = DownloadStage.INSTALLING)\n\n        val ext = assetName.substringAfterLast('.', \"\").lowercase()\n        val isApk = ext == \"apk\"\n\n        if (isApk) {\n            val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(filePath)\n            if (apkInfo == null) {\n                logger.error(\"Failed to extract APK info for $assetName\")\n                _state.value = _state.value.copy(\n                    downloadStage = DownloadStage.IDLE,\n                    installError = \"Failed to verify APK package info\",\n                )\n                currentAssetName = null\n                appendLog(\n                    assetName = assetName,\n                    size = sizeBytes,\n                    tag = releaseTag,\n                    result = Error(\"Failed to extract APK info\"),\n                )\n                return\n            }\n\n            // Validate package name matches on updates\n            val trackedApp = _state.value.installedApp\n            if (isUpdate && trackedApp != null && apkInfo.packageName != trackedApp.packageName) {\n                logger.error(\"Package name mismatch on update: APK=${apkInfo.packageName}, installed=${trackedApp.packageName}\")\n                _state.value = _state.value.copy(\n                    downloadStage = DownloadStage.IDLE,\n                    installError = getString(\n                        Res.string.update_package_mismatch,\n                        apkInfo.packageName,\n                        trackedApp.packageName,\n                    ),\n                )\n                currentAssetName = null\n                appendLog(\n                    assetName = assetName,\n                    size = sizeBytes,\n                    tag = releaseTag,\n                    result = Error(\"Package name mismatch\"),\n                )\n                return\n            }\n\n            val result =\n                checkFingerprints(\n                    apkPackageInfo = apkInfo,\n                )\n\n            result\n                .onFailure {\n                    val existingApp =\n                        installedAppsRepository.getAppByPackage(apkInfo.packageName)\n                    _state.update { state ->\n                        state.copy(\n                            signingKeyWarning =\n                                SigningKeyWarning(\n                                    packageName = apkInfo.packageName,\n                                    expectedFingerprint = existingApp?.signingFingerprint ?: \"\",\n                                    actualFingerprint = apkInfo.signingFingerprint ?: \"\",\n                                    pendingDownloadUrl = downloadUrl,\n                                    pendingAssetName = assetName,\n                                    pendingSizeBytes = sizeBytes,\n                                    pendingReleaseTag = releaseTag,\n                                    pendingIsUpdate = isUpdate,\n                                    pendingFilePath = filePath,\n                                ),\n                        )\n                    }\n                    appendLog(\n                        assetName = assetName,\n                        size = sizeBytes,\n                        tag = releaseTag,\n                        result = Error(\"Signing key changed\"),\n                    )\n                    return\n                }\n        }\n\n        installer.install(filePath, ext)\n\n        // Launch attestation check asynchronously (non-blocking)\n        launchAttestationCheck(filePath)\n\n        if (platform == Platform.ANDROID) {\n            saveInstalledAppToDatabase(\n                assetName = assetName,\n                assetUrl = downloadUrl,\n                assetSize = sizeBytes,\n                releaseTag = releaseTag,\n                isUpdate = isUpdate,\n                filePath = filePath,\n            )\n        } else {\n            viewModelScope.launch {\n                _events.send(DetailsEvent.OnMessage(getString(Res.string.installer_saved_downloads)))\n            }\n        }\n\n        _state.value = _state.value.copy(downloadStage = DownloadStage.IDLE)\n        currentAssetName = null\n        appendLog(\n            assetName = assetName,\n            size = sizeBytes,\n            tag = releaseTag,\n            result =\n                if (isUpdate) {\n                    LogResult.Updated\n                } else {\n                    LogResult.Installed\n                },\n        )\n    }\n\n    private suspend fun checkFingerprints(apkPackageInfo: ApkPackageInfo): Result<Unit> {\n        val existingApp =\n            installedAppsRepository.getAppByPackage(apkPackageInfo.packageName)\n                ?: return Result.success(Unit)\n\n        if (existingApp.signingFingerprint == null) return Result.success(Unit)\n\n        if (apkPackageInfo.signingFingerprint == null) return Result.success(Unit)\n\n        return if (existingApp.signingFingerprint == apkPackageInfo.signingFingerprint) {\n            Result.success(Unit)\n        } else {\n            Result.failure(\n                IllegalStateException(\n                    \"Signing key changed! Expected: ${existingApp.signingFingerprint}, got: ${apkPackageInfo.signingFingerprint}\",\n                ),\n            )\n        }\n    }\n\n    private fun launchAttestationCheck(filePath: String) {\n        val repo = _state.value.repository ?: return\n        val owner = repo.owner.login\n        val repoName = repo.name\n\n        _state.update { it.copy(attestationStatus = AttestationStatus.CHECKING) }\n\n        viewModelScope.launch {\n            try {\n                val digest = computeSha256(filePath)\n                val verified = detailsRepository.checkAttestations(owner, repoName, digest)\n                _state.update {\n                    it.copy(\n                        attestationStatus =\n                            if (verified) AttestationStatus.VERIFIED else AttestationStatus.UNVERIFIED,\n                    )\n                }\n            } catch (e: Exception) {\n                logger.debug(\"Attestation check error: ${e.message}\")\n                _state.update { it.copy(attestationStatus = AttestationStatus.UNVERIFIED) }\n            }\n        }\n    }\n\n    private fun computeSha256(filePath: String): String {\n        val digest = MessageDigest.getInstance(\"SHA-256\")\n        val buffer = ByteArray(8192)\n        FileInputStream(File(filePath)).use { fis ->\n            var bytesRead: Int\n            while (fis.read(buffer).also { bytesRead = it } != -1) {\n                digest.update(buffer, 0, bytesRead)\n            }\n        }\n        return digest.digest().joinToString(\"\") { \"%02x\".format(it) }\n    }\n\n    private suspend fun downloadAsset(\n        assetName: String,\n        sizeBytes: Long,\n        releaseTag: String,\n        isUpdate: Boolean,\n        downloadUrl: String,\n    ): String? {\n        currentAssetName = assetName\n\n        appendLog(\n            assetName = assetName,\n            size = sizeBytes,\n            tag = releaseTag,\n            result =\n                if (isUpdate) {\n                    LogResult.UpdateStarted\n                } else {\n                    LogResult.DownloadStarted\n                },\n        )\n        _state.value =\n            _state.value.copy(\n                downloadError = null,\n                installError = null,\n                downloadProgressPercent = null,\n                attestationStatus = AttestationStatus.UNCHECKED,\n            )\n\n        val existingPath = downloader.getDownloadedFilePath(assetName)\n        val filePath: String\n\n        val existingFile = existingPath?.let { File(it) }\n        if (existingFile != null && existingFile.exists() && existingFile.length() == sizeBytes) {\n            logger.debug(\"Reusing already downloaded file: $assetName\")\n            filePath = existingPath\n            _state.value =\n                _state.value.copy(\n                    downloadProgressPercent = 100,\n                    downloadedBytes = sizeBytes,\n                    totalBytes = sizeBytes,\n                    downloadStage = DownloadStage.VERIFYING,\n                )\n        } else {\n            _state.value =\n                _state.value.copy(\n                    downloadStage = DownloadStage.DOWNLOADING,\n                    downloadedBytes = 0L,\n                    totalBytes = sizeBytes,\n                )\n            downloader.download(downloadUrl, assetName).collect { p ->\n                _state.value =\n                    _state.value.copy(\n                        downloadProgressPercent = p.percent,\n                        downloadedBytes = p.bytesDownloaded,\n                        totalBytes = p.totalBytes ?: sizeBytes,\n                    )\n                if (p.percent == 100) {\n                    _state.value =\n                        _state.value.copy(downloadStage = DownloadStage.VERIFYING)\n                }\n            }\n\n            filePath = downloader.getDownloadedFilePath(assetName)\n                ?: throw IllegalStateException(\"Downloaded file not found\")\n\n            cachedDownloadAssetName = assetName\n        }\n\n        appendLog(\n            assetName = assetName,\n            size = sizeBytes,\n            tag = releaseTag,\n            result = LogResult.Downloaded,\n        )\n        val ext = assetName.substringAfterLast('.', \"\").lowercase()\n\n        if (!installer.isSupported(ext)) {\n            throw IllegalStateException(\"Asset type .$ext not supported\")\n        }\n\n        try {\n            installer.ensurePermissionsOrThrow(extOrMime = ext)\n        } catch (e: IllegalStateException) {\n            logger.warn(\"Install permission blocked: ${e.message}\")\n            _state.value =\n                _state.value.copy(\n                    downloadStage = DownloadStage.IDLE,\n                    showExternalInstallerPrompt = true,\n                    pendingInstallFilePath = filePath,\n                )\n            currentAssetName = null\n            appendLog(\n                assetName = assetName,\n                size = sizeBytes,\n                tag = releaseTag,\n                result = LogResult.PermissionBlocked,\n            )\n            return null\n        }\n\n        return filePath\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private suspend fun saveInstalledAppToDatabase(\n        assetName: String,\n        assetUrl: String,\n        assetSize: Long,\n        releaseTag: String,\n        isUpdate: Boolean,\n        filePath: String,\n    ) {\n        try {\n            val repo = _state.value.repository ?: return\n\n            val apkInfo: ApkPackageInfo =\n                if (platform == Platform.ANDROID && assetName.lowercase().endsWith(\".apk\")) {\n                    val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(filePath)\n                    if (apkInfo != null) {\n                        ApkPackageInfo(\n                            packageName = apkInfo.packageName,\n                            appName = apkInfo.appName,\n                            versionName = apkInfo.versionName,\n                            versionCode = apkInfo.versionCode,\n                            signingFingerprint = apkInfo.signingFingerprint,\n                        )\n                    } else {\n                        logger.error(\"Failed to extract APK info for $assetName\")\n                        return\n                    }\n                } else {\n                    return\n                }\n\n            if (isUpdate) {\n                installedAppsRepository.updateAppVersion(\n                    packageName = apkInfo.packageName,\n                    newTag = releaseTag,\n                    newAssetName = assetName,\n                    newAssetUrl = assetUrl,\n                    newVersionName = apkInfo.versionName,\n                    newVersionCode = apkInfo.versionCode,\n                    signingFingerprint = apkInfo.signingFingerprint,\n                )\n            } else {\n                val installedApp =\n                    InstalledApp(\n                        packageName = apkInfo.packageName,\n                        repoId = repo.id,\n                        repoName = repo.name,\n                        repoOwner = repo.owner.login,\n                        repoOwnerAvatarUrl = repo.owner.avatarUrl,\n                        repoDescription = repo.description,\n                        primaryLanguage = repo.language,\n                        repoUrl = repo.htmlUrl,\n                        installedVersion = releaseTag,\n                        installedAssetName = assetName,\n                        installedAssetUrl = assetUrl,\n                        latestVersion = releaseTag,\n                        latestAssetName = assetName,\n                        latestAssetUrl = assetUrl,\n                        latestAssetSize = assetSize,\n                        appName = apkInfo.appName,\n                        installSource = InstallSource.THIS_APP,\n                        installedAt = System.now().toEpochMilliseconds(),\n                        lastCheckedAt = System.now().toEpochMilliseconds(),\n                        lastUpdatedAt = System.now().toEpochMilliseconds(),\n                        isUpdateAvailable = false,\n                        updateCheckEnabled = true,\n                        releaseNotes = \"\",\n                        systemArchitecture = installer.detectSystemArchitecture().name,\n                        fileExtension = assetName.substringAfterLast('.', \"\"),\n                        isPendingInstall = true,\n                        installedVersionName = apkInfo.versionName,\n                        installedVersionCode = apkInfo.versionCode,\n                        latestVersionName = apkInfo.versionName,\n                        latestVersionCode = apkInfo.versionCode,\n                        signingFingerprint = apkInfo.signingFingerprint,\n                    )\n\n                installedAppsRepository.saveInstalledApp(installedApp)\n            }\n\n            if (_state.value.isFavourite) {\n                favouritesRepository.updateFavoriteInstallStatus(\n                    repoId = repo.id,\n                    installed = true,\n                    packageName = apkInfo.packageName,\n                )\n            }\n\n            delay(1000)\n            val updatedApp = installedAppsRepository.getAppByPackage(apkInfo.packageName)\n            _state.value = _state.value.copy(installedApp = updatedApp)\n\n            logger.debug(\"Successfully saved and reloaded app: ${updatedApp?.packageName}\")\n        } catch (t: Throwable) {\n            logger.error(\"Failed to save installed app to database: ${t.message}\")\n            t.printStackTrace()\n        }\n    }\n\n    private fun downloadAsset(\n        downloadUrl: String,\n        assetName: String,\n        sizeBytes: Long,\n        releaseTag: String,\n    ) {\n        currentDownloadJob?.cancel()\n        currentDownloadJob =\n            viewModelScope.launch {\n                try {\n                    currentAssetName = assetName\n\n                    appendLog(\n                        assetName = assetName,\n                        size = sizeBytes,\n                        tag = releaseTag,\n                        result = LogResult.DownloadStarted,\n                    )\n                    _state.value =\n                        _state.value.copy(\n                            isDownloading = true,\n                            downloadError = null,\n                            installError = null,\n                            downloadProgressPercent = null,\n                        )\n\n                    downloader.download(downloadUrl, assetName).collect { p ->\n                        _state.value = _state.value.copy(downloadProgressPercent = p.percent)\n                    }\n\n                    _state.value = _state.value.copy(isDownloading = false)\n                    currentAssetName = null\n                    appendLog(\n                        assetName = assetName,\n                        size = sizeBytes,\n                        tag = releaseTag,\n                        result = LogResult.Downloaded,\n                    )\n                } catch (t: Throwable) {\n                    _state.value =\n                        _state.value.copy(\n                            isDownloading = false,\n                            downloadError = t.message,\n                        )\n                    currentAssetName = null\n                    appendLog(\n                        assetName = assetName,\n                        size = sizeBytes,\n                        tag = releaseTag,\n                        result = LogResult.Error(t.message),\n                    )\n                }\n            }\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private fun appendLog(\n        assetName: String,\n        size: Long,\n        tag: String,\n        result: LogResult,\n    ) {\n        val now =\n            System\n                .now()\n                .toLocalDateTime(TimeZone.currentSystemDefault())\n                .format(\n                    LocalDateTime.Format {\n                        year()\n                        char('-')\n                        monthNumber()\n                        char('-')\n                        day()\n                        char(' ')\n                        hour()\n                        char(':')\n                        minute()\n                        char(':')\n                        second()\n                    },\n                )\n        val newItem =\n            InstallLogItem(\n                timeIso = now,\n                assetName = assetName,\n                assetSizeBytes = size,\n                releaseTag = tag,\n                result = result,\n            )\n        _state.value =\n            _state.value.copy(\n                installLogs = listOf(newItem) + _state.value.installLogs,\n            )\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        currentDownloadJob?.cancel()\n\n        val assetsToClean = listOfNotNull(currentAssetName, cachedDownloadAssetName).distinct()\n        if (assetsToClean.isNotEmpty()) {\n            viewModelScope.launch(NonCancellable) {\n                for (asset in assetsToClean) {\n                    try {\n                        downloader.cancelDownload(asset)\n                        logger.debug(\"Cleaned up download on screen leave: $asset\")\n                    } catch (t: Throwable) {\n                        logger.error(\"Failed to clean download on leave: ${t.message}\")\n                    }\n                }\n            }\n        }\n    }\n\n    private fun translateContent(\n        text: String,\n        targetLanguageCode: String,\n        updateState: (TranslationState) -> Unit,\n        getCurrentState: () -> TranslationState,\n    ): Job =\n        viewModelScope.launch {\n            try {\n                updateState(\n                    getCurrentState().copy(\n                        isTranslating = true,\n                        error = null,\n                        targetLanguageCode = targetLanguageCode,\n                    ),\n                )\n\n                val result =\n                    translationRepository.translate(\n                        text = text,\n                        targetLanguage = targetLanguageCode,\n                    )\n\n                val langDisplayName =\n                    SupportedLanguages.all\n                        .find { it.code == targetLanguageCode }\n                        ?.displayName\n                        ?: targetLanguageCode\n\n                updateState(\n                    TranslationState(\n                        isTranslating = false,\n                        translatedText = result.translatedText,\n                        isShowingTranslation = true,\n                        targetLanguageCode = targetLanguageCode,\n                        targetLanguageDisplayName = langDisplayName,\n                        detectedSourceLanguage = result.detectedSourceLanguage,\n                    ),\n                )\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                logger.error(\"Translation failed: ${e.message}\")\n                updateState(\n                    getCurrentState().copy(\n                        isTranslating = false,\n                        error = e.message,\n                    ),\n                )\n                _events.send(\n                    DetailsEvent.OnMessage(getString(Res.string.translation_failed)),\n                )\n            }\n        }\n\n    private fun normalizeVersion(version: String?): String = version?.removePrefix(\"v\")?.removePrefix(\"V\")?.trim() ?: \"\"\n\n    /**\n     * Returns true if [candidate] is strictly older than [current].\n     * Uses list-index order as primary heuristic (releases are newest-first),\n     * and falls back to semantic version comparison when list lookup fails.\n     */\n    private fun isDowngradeVersion(\n        candidate: String,\n        current: String,\n        allReleases: List<GithubRelease>,\n    ): Boolean {\n        val normalizedCandidate = normalizeVersion(candidate)\n        val normalizedCurrent = normalizeVersion(current)\n\n        if (normalizedCandidate == normalizedCurrent) return false\n\n        val candidateIndex =\n            allReleases.indexOfFirst {\n                normalizeVersion(it.tagName) == normalizedCandidate\n            }\n        val currentIndex =\n            allReleases.indexOfFirst {\n                normalizeVersion(it.tagName) == normalizedCurrent\n            }\n\n        if (candidateIndex != -1 && currentIndex != -1) {\n            return candidateIndex > currentIndex\n        }\n\n        return compareSemanticVersions(normalizedCandidate, normalizedCurrent) < 0\n    }\n\n    /**\n     * Compares two semantic version strings. Returns positive if a > b, negative if a < b, 0 if equal.\n     */\n    private fun compareSemanticVersions(\n        a: String,\n        b: String,\n    ): Int {\n        val aCore = a.split(\"-\", limit = 2)\n        val bCore = b.split(\"-\", limit = 2)\n        val aParts = aCore[0].split(\".\")\n        val bParts = bCore[0].split(\".\")\n\n        val maxLen = maxOf(aParts.size, bParts.size)\n        for (i in 0 until maxLen) {\n            val aPart = aParts.getOrNull(i)?.filter { it.isDigit() }?.toLongOrNull() ?: 0L\n            val bPart = bParts.getOrNull(i)?.filter { it.isDigit() }?.toLongOrNull() ?: 0L\n            if (aPart != bPart) return aPart.compareTo(bPart)\n        }\n\n        val aHasPre = aCore.size > 1\n        val bHasPre = bCore.size > 1\n        if (aHasPre != bHasPre) return if (aHasPre) -1 else 1\n\n        return 0\n    }\n\n    private companion object {\n        const val OBTAINIUM_REPO_ID: Long = 523534328\n        const val APP_MANAGER_REPO_ID: Long = 268006778\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.Schedule\nimport androidx.compose.material.icons.filled.Update\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.coil3.CoilImage\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.GithubUserProfile\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.presentation.components.ForkBadge\nimport zed.rainxch.core.presentation.components.PlatformChip\nimport zed.rainxch.core.presentation.utils.formatReleasedAt\nimport zed.rainxch.details.presentation.model.DownloadStage\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.by_author\nimport zed.rainxch.githubstore.core.presentation.res.installed\nimport zed.rainxch.githubstore.core.presentation.res.installed_version\nimport zed.rainxch.githubstore.core.presentation.res.no_description\nimport zed.rainxch.githubstore.core.presentation.res.pending_install\nimport zed.rainxch.githubstore.core.presentation.res.update_available\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class)\n@Composable\nfun AppHeader(\n    author: GithubUserProfile?,\n    repository: GithubRepoSummary,\n    release: GithubRelease?,\n    installedApp: InstalledApp?,\n    modifier: Modifier = Modifier,\n    downloadStage: DownloadStage = DownloadStage.IDLE,\n    downloadProgress: Int? = null,\n) {\n    val animatedProgress by animateFloatAsState(\n        targetValue = (downloadProgress ?: 0) / 100f,\n        animationSpec = tween(durationMillis = 500),\n        label = \"avatar_progress_animation\",\n    )\n\n    val supportedPlatforms by remember(release?.assets) {\n        derivedStateOf {\n            derivePlatformsFromAssets(release)\n        }\n    }\n\n    Column(\n        modifier = modifier.fillMaxWidth(),\n    ) {\n        Row(\n            verticalAlignment = Alignment.Top,\n        ) {\n            Box(\n                contentAlignment = Alignment.Center,\n                modifier = Modifier.size(100.dp),\n            ) {\n                CoilImage(\n                    imageModel = { author?.avatarUrl },\n                    modifier =\n                        Modifier\n                            .size(100.dp)\n                            .clip(CircleShape)\n                            .border(\n                                width = 1.dp,\n                                color = MaterialTheme.colorScheme.outlineVariant,\n                                shape = CircleShape,\n                            ),\n                    loading = {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            CircularWavyProgressIndicator()\n                        }\n                    },\n                )\n\n                if (downloadStage != DownloadStage.IDLE) {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier = Modifier.size(100.dp),\n                    ) {\n                        when (downloadStage) {\n                            DownloadStage.DOWNLOADING -> {\n                                CircularProgressIndicator(\n                                    progress = { 1f },\n                                    modifier = Modifier.fillMaxSize(),\n                                    color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),\n                                    strokeWidth = 4.dp,\n                                )\n\n                                CircularProgressIndicator(\n                                    progress = { animatedProgress },\n                                    modifier = Modifier.fillMaxSize(),\n                                    color = MaterialTheme.colorScheme.primary,\n                                    strokeWidth = 4.dp,\n                                    strokeCap = StrokeCap.Round,\n                                )\n                            }\n\n                            DownloadStage.VERIFYING, DownloadStage.INSTALLING -> {\n                                CircularProgressIndicator(\n                                    modifier = Modifier.fillMaxSize(),\n                                    color = MaterialTheme.colorScheme.primary,\n                                    strokeWidth = 4.dp,\n                                    strokeCap = StrokeCap.Round,\n                                )\n                            }\n\n                            else -> {}\n                        }\n                    }\n                }\n            }\n\n            Spacer(Modifier.width(16.dp))\n\n            Column(\n                modifier = Modifier.weight(1f),\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Text(\n                        text = repository.name,\n                        style = MaterialTheme.typography.headlineSmall,\n                        fontWeight = FontWeight.Bold,\n                        color = MaterialTheme.colorScheme.onBackground,\n                        modifier = Modifier.weight(1f, fill = false),\n                    )\n\n                    if (repository.isFork) {\n                        ForkBadge()\n                    }\n                }\n                author?.login?.let { author ->\n                    Text(\n                        text = stringResource(Res.string.by_author, author),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                }\n\n                Spacer(Modifier.height(8.dp))\n\n                if (installedApp != null) {\n                    when {\n                        installedApp.isPendingInstall -> {\n                            PendingInstallBadge()\n                        }\n\n                        else -> {\n                            InstallStatusBadge(\n                                isUpdateAvailable = installedApp.isUpdateAvailable,\n                            )\n                        }\n                    }\n                }\n\n                Spacer(Modifier.height(8.dp))\n\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    release?.tagName?.let {\n                        Text(\n                            text = it,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.secondary,\n                        )\n                    }\n\n                    if (installedApp != null && installedApp.installedVersion != release?.tagName) {\n                        Text(\n                            text =\n                                stringResource(\n                                    Res.string.installed_version,\n                                    installedApp.installedVersion,\n                                ),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n\n                release?.publishedAt?.let { publishedAt ->\n                    Spacer(Modifier.height(4.dp))\n\n                    Text(\n                        text = formatReleasedAt(publishedAt),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.outline,\n                    )\n                }\n            }\n        }\n\n        if (supportedPlatforms.isNotEmpty()) {\n            Spacer(Modifier.height(12.dp))\n\n            FlowRow(\n                horizontalArrangement = Arrangement.spacedBy(6.dp),\n                verticalArrangement = Arrangement.spacedBy(6.dp),\n            ) {\n                supportedPlatforms.forEach { platform ->\n                    PlatformChip(platform = platform)\n                }\n            }\n        }\n\n        Spacer(Modifier.height(16.dp))\n\n        Text(\n            text = repository.description ?: stringResource(Res.string.no_description),\n            style = MaterialTheme.typography.bodyLarge,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n    }\n}\n\nprivate fun derivePlatformsFromAssets(release: GithubRelease?): List<DiscoveryPlatform> {\n    if (release == null) return emptyList()\n    val names = release.assets.map { it.name.lowercase() }\n    return buildList {\n        if (names.any { it.endsWith(\".apk\") }) add(DiscoveryPlatform.Android)\n        if (names.any { it.endsWith(\".exe\") || it.endsWith(\".msi\") }) add(DiscoveryPlatform.Windows)\n        if (names.any { it.endsWith(\".dmg\") || it.endsWith(\".pkg\") }) add(DiscoveryPlatform.Macos)\n        if (names.any { it.endsWith(\".appimage\") || it.endsWith(\".deb\") || it.endsWith(\".rpm\") }) {\n            add(\n                DiscoveryPlatform.Linux,\n            )\n        }\n    }\n}\n\n@Composable\nfun InstallStatusBadge(\n    isUpdateAvailable: Boolean,\n    modifier: Modifier = Modifier,\n) {\n    val backgroundColor =\n        if (isUpdateAvailable) {\n            MaterialTheme.colorScheme.tertiaryContainer\n        } else {\n            MaterialTheme.colorScheme.primaryContainer\n        }\n\n    val textColor =\n        if (isUpdateAvailable) {\n            MaterialTheme.colorScheme.onTertiaryContainer\n        } else {\n            MaterialTheme.colorScheme.onPrimaryContainer\n        }\n\n    val icon =\n        if (isUpdateAvailable) {\n            Icons.Default.Update\n        } else {\n            Icons.Default.CheckCircle\n        }\n\n    val text =\n        if (isUpdateAvailable) {\n            stringResource(Res.string.update_available)\n        } else {\n            stringResource(Res.string.installed)\n        }\n\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        color = backgroundColor,\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier = Modifier.size(14.dp),\n                tint = textColor,\n            )\n            Text(\n                text = text,\n                style = MaterialTheme.typography.labelSmall,\n                color = textColor,\n                fontWeight = FontWeight.SemiBold,\n            )\n        }\n    }\n}\n\n@Composable\nfun PendingInstallBadge(modifier: Modifier = Modifier) {\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(12.dp),\n        color = MaterialTheme.colorScheme.secondaryContainer,\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = Icons.Default.Schedule,\n                contentDescription = null,\n                modifier = Modifier.size(14.dp),\n                tint = MaterialTheme.colorScheme.onSecondaryContainer,\n            )\n            Text(\n                text = stringResource(Res.string.pending_install),\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.onSecondaryContainer,\n                fontWeight = FontWeight.SemiBold,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.filled.Smartphone\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.domain.model.SupportedLanguage\nimport zed.rainxch.details.presentation.model.SupportedLanguages\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LanguagePicker(\n    isVisible: Boolean,\n    selectedLanguageCode: String?,\n    deviceLanguageCode: String,\n    onLanguageSelected: (SupportedLanguage) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    if (!isVisible) return\n\n    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)\n    var searchQuery by remember { mutableStateOf(\"\") }\n\n    val deviceLanguage = remember(deviceLanguageCode) {\n        SupportedLanguages.all.find { it.code == deviceLanguageCode }\n    }\n\n    val filteredLanguages =\n        remember(searchQuery) {\n            val all = SupportedLanguages.all\n            if (searchQuery.isBlank()) {\n                all\n            } else {\n                all.filter {\n                    it.displayName.contains(searchQuery, ignoreCase = true) ||\n                        it.code.contains(searchQuery, ignoreCase = true)\n                }\n            }\n        }\n\n    ModalBottomSheet(\n        onDismissRequest = onDismiss,\n        sheetState = sheetState,\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .navigationBarsPadding(),\n        ) {\n            Text(\n                text = stringResource(Res.string.translate_to),\n                style = MaterialTheme.typography.titleMedium,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n            )\n\n            OutlinedTextField(\n                value = searchQuery,\n                onValueChange = { searchQuery = it },\n                placeholder = { Text(stringResource(Res.string.search_language)) },\n                leadingIcon = {\n                    Icon(Icons.Default.Search, contentDescription = null)\n                },\n                singleLine = true,\n                shape = RoundedCornerShape(12.dp),\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp),\n            )\n\n            // Device language shortcut — only shown when not searching\n            if (searchQuery.isBlank() && deviceLanguage != null) {\n                Row(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 16.dp, vertical = 4.dp)\n                            .clip(RoundedCornerShape(12.dp))\n                            .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f))\n                            .clickable { onLanguageSelected(deviceLanguage) }\n                            .padding(horizontal = 12.dp, vertical = 10.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Smartphone,\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.primary,\n                        modifier = Modifier.size(18.dp),\n                    )\n                    Spacer(Modifier.width(10.dp))\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = deviceLanguage.displayName,\n                            style = MaterialTheme.typography.titleSmall,\n                            fontWeight = FontWeight.SemiBold,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                        Text(\n                            text = stringResource(Res.string.select_language),\n                            style = MaterialTheme.typography.labelSmall,\n                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),\n                        )\n                    }\n                    if (deviceLanguage.code == selectedLanguageCode) {\n                        Icon(\n                            imageVector = Icons.Default.CheckCircle,\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.size(20.dp),\n                        )\n                    }\n                }\n\n                Spacer(Modifier.height(4.dp))\n            }\n\n            HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n\n            LazyColumn(\n                modifier = Modifier.fillMaxWidth(),\n                contentPadding = PaddingValues(vertical = 8.dp),\n            ) {\n                items(\n                    items = filteredLanguages,\n                    key = { it.code },\n                ) { language ->\n                    LanguageListItem(\n                        language = language,\n                        isSelected = language.code == selectedLanguageCode,\n                        onClick = { onLanguageSelected(language) },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LanguageListItem(\n    language: SupportedLanguage,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n) {\n    Row(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .clickable(onClick = onClick)\n                .padding(horizontal = 16.dp, vertical = 12.dp),\n        horizontalArrangement = Arrangement.SpaceBetween,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = language.displayName,\n                style = MaterialTheme.typography.titleSmall,\n                fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                color =\n                    if (isSelected) {\n                        MaterialTheme.colorScheme.primary\n                    } else {\n                        MaterialTheme.colorScheme.onSurface\n                    },\n            )\n            Text(\n                text = language.code,\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.outline,\n            )\n        }\n\n        if (isSelected) {\n            Spacer(Modifier.width(8.dp))\n            Icon(\n                imageVector = Icons.Default.CheckCircle,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.size(20.dp),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.UnfoldMore\nimport androidx.compose.material.icons.outlined.Info\nimport androidx.compose.material3.AlertDialogDefaults\nimport androidx.compose.material3.BasicAlertDialog\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedCard\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.GithubUser\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.githubstore.core.presentation.res.Res\n\n@OptIn(\n    ExperimentalMaterial3Api::class,\n    ExperimentalMaterial3ExpressiveApi::class,\n)\n@Composable\nfun ReleaseAssetsPicker(\n    onAction: (DetailsAction) -> Unit,\n    assetsList: List<GithubAsset>,\n    modifier: Modifier = Modifier,\n    selectedAsset: GithubAsset? = null,\n    isPickerVisible: Boolean = false,\n) {\n    val isPickerEnabled by remember(assetsList) {\n        derivedStateOf { assetsList.isNotEmpty() }\n    }\n\n    ReleaseAssetsItemsPicker(\n        showPicker = isPickerVisible,\n        assetsList = assetsList,\n        selectedAsset = selectedAsset,\n        onDismiss = { onAction(DetailsAction.ToggleReleaseAssetsPicker) },\n        onSelect = { onAction(DetailsAction.SelectDownloadAsset(it)) },\n    )\n\n    Column(\n        modifier = modifier.wrapContentHeight(),\n        verticalArrangement = Arrangement.spacedBy(4.dp),\n    ) {\n        Text(\n            text = stringResource(Res.string.assets_title),\n            style = MaterialTheme.typography.labelLargeEmphasized,\n            color = MaterialTheme.colorScheme.tertiary,\n            modifier = Modifier.padding(horizontal = 4.dp),\n        )\n        OutlinedCard(\n            onClick = { onAction(DetailsAction.ToggleReleaseAssetsPicker) },\n            enabled = isPickerEnabled,\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 12.dp)\n                        .heightIn(min = 36.dp),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Text(\n                    text = selectedAsset?.name ?: stringResource(Res.string.no_assets_selected),\n                    style = MaterialTheme.typography.titleSmall,\n                    fontWeight = FontWeight.SemiBold,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                    modifier = Modifier.weight(1f),\n                )\n                Icon(\n                    imageVector = Icons.Default.UnfoldMore,\n                    contentDescription = stringResource(Res.string.select_version),\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ReleaseAssetsItemsPicker(\n    assetsList: List<GithubAsset>,\n    selectedAsset: GithubAsset?,\n    showPicker: Boolean,\n    onDismiss: () -> Unit,\n    onSelect: (GithubAsset) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    if (!showPicker) return\n\n    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)\n    var showInfoDialog by rememberSaveable { mutableStateOf(false) }\n\n    ReleaseAssetsAboutDialog(\n        showDialog = showInfoDialog,\n        onDismiss = { showInfoDialog = false },\n    )\n\n    ModalBottomSheet(\n        onDismissRequest = onDismiss,\n        sheetState = sheetState,\n        modifier = modifier,\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .navigationBarsPadding(),\n        ) {\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                Text(\n                    text = stringResource(Res.string.assets_title),\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.Bold,\n                    modifier =\n                        Modifier\n                            .padding(horizontal = 16.dp, vertical = 8.dp)\n                            .weight(1f),\n                )\n                IconButton(onClick = { showInfoDialog = true }) {\n                    Icon(imageVector = Icons.Outlined.Info, contentDescription = stringResource(Res.string.icon_content_description_info))\n                }\n            }\n\n            HorizontalDivider()\n\n            LazyColumn(\n                modifier = Modifier.fillMaxWidth(),\n                contentPadding = PaddingValues(vertical = 8.dp),\n            ) {\n                if (assetsList.isNotEmpty()) {\n                    items(items = assetsList, key = { it.id }) { asset ->\n                        ReleaseAssetItem(\n                            asset = asset,\n                            isSelected = asset.id == selectedAsset?.id,\n                            onClick = { onSelect(asset) },\n                            modifier = Modifier.fillMaxWidth(),\n                        )\n                    }\n                } else {\n                    item {\n                        Text(\n                            text = stringResource(Res.string.no_assets_in_list),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            textAlign = TextAlign.Center,\n                            modifier = Modifier.fillMaxWidth().padding(16.dp),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ReleaseAssetsAboutDialog(\n    showDialog: Boolean,\n    onDismiss: () -> Unit,\n    modifier: Modifier = Modifier,\n    properties: DialogProperties = DialogProperties(),\n    containerColor: Color = AlertDialogDefaults.containerColor,\n    shape: Shape = AlertDialogDefaults.shape,\n) {\n    if (!showDialog) return\n\n    BasicAlertDialog(onDismissRequest = onDismiss, modifier = modifier, properties = properties) {\n        Surface(\n            color = containerColor,\n            contentColor = contentColorFor(containerColor),\n            shape = shape,\n        ) {\n            Column(\n                modifier = Modifier.padding(24.dp),\n                verticalArrangement = Arrangement.spacedBy(16.dp),\n            ) {\n                Text(\n                    text = stringResource(Res.string.multiple_assets_info_dialog_title),\n                    style = MaterialTheme.typography.headlineSmall,\n                    color = AlertDialogDefaults.titleContentColor,\n                )\n                Text(\n                    text = stringResource(Res.string.multiple_assets_info_dialog_text),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = AlertDialogDefaults.textContentColor,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ReleaseAssetItem(\n    asset: GithubAsset,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier =\n            modifier\n                .clickable(\n                    onClickLabel = stringResource(Res.string.assets_selection_label),\n                    onClick = onClick,\n                ).padding(horizontal = 16.dp, vertical = 12.dp),\n        horizontalArrangement = Arrangement.SpaceBetween,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = asset.name,\n                style = MaterialTheme.typography.titleSmall,\n                fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                overflow = TextOverflow.Ellipsis,\n                maxLines = 2,\n                color =\n                    if (isSelected) {\n                        MaterialTheme.colorScheme.primary\n                    } else {\n                        MaterialTheme.colorScheme.onSurface\n                    },\n            )\n            Text(\n                text = formatFileSize(asset.size),\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n        if (isSelected) {\n            Spacer(Modifier.width(8.dp))\n            Icon(\n                imageVector = Icons.Default.CheckCircle,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.size(20.dp),\n            )\n        }\n    }\n}\n\nprivate fun formatFileSize(bytes: Long): String =\n    when {\n        bytes >= 1_073_741_824 -> \"%.1f GB\".format(bytes / 1_073_741_824.0)\n        bytes >= 1_048_576 -> \"%.1f MB\".format(bytes / 1_048_576.0)\n        bytes >= 1_024 -> \"%.1f KB\".format(bytes / 1_024.0)\n        else -> \"$bytes B\"\n    }\n\n@Preview\n@Composable\nprivate fun ReleaseAssetsPickerItemPreview() {\n    ReleaseAssetItem(\n        asset =\n            GithubAsset(\n                id = -1,\n                name = \"Random\",\n                contentType = \"\",\n                size = 20 * 1024,\n                downloadUrl = \"\",\n                uploader = GithubUser(id = -1, login = \"\", avatarUrl = \"\", htmlUrl = \"\"),\n            ),\n        onClick = {},\n        isSelected = false,\n    )\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.OpenInNew\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.KeyboardArrowDown\nimport androidx.compose.material.icons.filled.Update\nimport androidx.compose.material.icons.filled.VerifiedUser\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport io.github.fletchmckee.liquid.liquefiable\nimport io.github.fletchmckee.liquid.rememberLiquidState\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.GithubAsset\nimport zed.rainxch.core.domain.model.GithubUser\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.details.presentation.DetailsState\nimport zed.rainxch.details.presentation.model.AttestationStatus\nimport zed.rainxch.details.presentation.model.DownloadStage\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.details.presentation.utils.extractArchitectureFromName\nimport zed.rainxch.details.presentation.utils.isExactArchitectureMatch\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.architecture_compatible\nimport zed.rainxch.githubstore.core.presentation.res.cancel_download\nimport zed.rainxch.githubstore.core.presentation.res.checking_attestation\nimport zed.rainxch.githubstore.core.presentation.res.downloading\nimport zed.rainxch.githubstore.core.presentation.res.install_latest\nimport zed.rainxch.githubstore.core.presentation.res.install_version\nimport zed.rainxch.githubstore.core.presentation.res.installing\nimport zed.rainxch.githubstore.core.presentation.res.not_available\nimport zed.rainxch.githubstore.core.presentation.res.open_app\nimport zed.rainxch.githubstore.core.presentation.res.show_install_options\nimport zed.rainxch.githubstore.core.presentation.res.uninstall\nimport zed.rainxch.githubstore.core.presentation.res.update_to_version\nimport zed.rainxch.githubstore.core.presentation.res.updating\nimport zed.rainxch.githubstore.core.presentation.res.verified_build\nimport zed.rainxch.githubstore.core.presentation.res.verifying\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SmartInstallButton(\n    isDownloading: Boolean,\n    isInstalling: Boolean,\n    isLiquidGlassEnabled: Boolean,\n    progress: Int?,\n    primaryAsset: GithubAsset?,\n    onAction: (DetailsAction) -> Unit,\n    modifier: Modifier = Modifier,\n    state: DetailsState,\n) {\n    val liquidState = LocalTopbarLiquidState.current\n\n    val installedApp = state.installedApp\n    val isInstalled = installedApp != null && !installedApp.isPendingInstall\n    val isUpdateAvailable =\n        installedApp?.isUpdateAvailable == true && !installedApp.isPendingInstall\n\n    val isSameVersionInstalled =\n        isInstalled &&\n            installedApp != null &&\n            normalizeVersion(installedApp.installedVersion) ==\n            normalizeVersion(\n                state.selectedRelease?.tagName ?: \"\",\n            )\n\n    val enabled =\n        remember(primaryAsset, isDownloading, isInstalling) {\n            primaryAsset != null && !isDownloading && !isInstalling\n        }\n\n    val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE\n\n    // When same version is installed, show Open button\n    if (isSameVersionInstalled && !isActiveDownload) {\n        Column(modifier = modifier) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(4.dp),\n            ) {\n                // Uninstall button\n                ElevatedCard(\n                    onClick = { onAction(DetailsAction.OnRequestUninstall) },\n                    modifier =\n                        Modifier\n                            .weight(1f)\n                            .height(52.dp)\n                            .then(\n                                if (isLiquidGlassEnabled) {\n                                    Modifier.liquefiable(liquidState)\n                                } else {\n                                    Modifier\n                                },\n                            ),\n                    colors =\n                        CardDefaults.elevatedCardColors(\n                            containerColor = MaterialTheme.colorScheme.errorContainer,\n                        ),\n                    shape =\n                        RoundedCornerShape(\n                            topStart = 24.dp,\n                            bottomStart = 24.dp,\n                            topEnd = 6.dp,\n                            bottomEnd = 6.dp,\n                        ),\n                ) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(6.dp),\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.Delete,\n                                contentDescription = null,\n                                modifier = Modifier.size(18.dp),\n                                tint = MaterialTheme.colorScheme.onErrorContainer,\n                            )\n                            Text(\n                                text = stringResource(Res.string.uninstall),\n                                color = MaterialTheme.colorScheme.onErrorContainer,\n                                fontWeight = FontWeight.Bold,\n                                style = MaterialTheme.typography.titleMedium,\n                            )\n                        }\n                    }\n                }\n\n                // Open button\n                ElevatedCard(\n                    modifier =\n                        Modifier\n                            .weight(1f)\n                            .height(52.dp)\n                            .then(\n                                if (isLiquidGlassEnabled) {\n                                    Modifier.liquefiable(liquidState)\n                                } else {\n                                    Modifier\n                                },\n                            ),\n                    colors =\n                        CardDefaults.elevatedCardColors(\n                            containerColor = MaterialTheme.colorScheme.primary,\n                        ),\n                    shape =\n                        RoundedCornerShape(\n                            topStart = 6.dp,\n                            bottomStart = 6.dp,\n                            topEnd = 24.dp,\n                            bottomEnd = 24.dp,\n                        ),\n                    onClick = {\n                        onAction(DetailsAction.OpenApp)\n                    },\n                ) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(6.dp),\n                        ) {\n                            Icon(\n                                imageVector = Icons.AutoMirrored.Filled.OpenInNew,\n                                contentDescription = null,\n                                modifier = Modifier.size(18.dp),\n                                tint = MaterialTheme.colorScheme.onPrimary,\n                            )\n                            Text(\n                                text = stringResource(Res.string.open_app),\n                                color = MaterialTheme.colorScheme.onPrimary,\n                                fontWeight = FontWeight.Bold,\n                                style = MaterialTheme.typography.titleMedium,\n                            )\n                        }\n                    }\n                }\n            }\n\n            AttestationBadge(attestationStatus = state.attestationStatus)\n        }\n        return\n    }\n\n    // Regular install/update button for all other cases\n    val buttonColor =\n        when {\n            !enabled && !isActiveDownload -> MaterialTheme.colorScheme.surfaceContainer\n            isUpdateAvailable -> MaterialTheme.colorScheme.tertiary\n            isInstalled -> MaterialTheme.colorScheme.secondary\n            else -> MaterialTheme.colorScheme.primary\n        }\n\n    val buttonText =\n        when {\n            !enabled && primaryAsset == null -> {\n                stringResource(Res.string.not_available)\n            }\n\n            isUpdateAvailable -> {\n                stringResource(\n                    Res.string.update_to_version,\n                    installedApp.latestVersion.toString(),\n                )\n            }\n\n            isInstalled && installedApp.installedVersion != state.selectedRelease?.tagName -> {\n                stringResource(\n                    Res.string.install_version,\n                    state.selectedRelease?.tagName ?: \"\",\n                )\n            }\n\n            else -> {\n                stringResource(Res.string.install_latest)\n            }\n        }\n\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(4.dp),\n    ) {\n        ElevatedCard(\n            modifier =\n                Modifier\n                    .weight(1f)\n                    .height(52.dp)\n                    .background(\n                        color = buttonColor,\n                        shape = CircleShape,\n                    ).clickable(\n                        enabled = enabled,\n                        onClick = {\n                            if (!state.isDownloading && state.downloadStage == DownloadStage.IDLE) {\n                                if (isUpdateAvailable) {\n                                    onAction(DetailsAction.UpdateApp)\n                                } else {\n                                    onAction(DetailsAction.InstallPrimary)\n                                }\n                            }\n                        },\n                    ).then(\n                        if (isLiquidGlassEnabled) {\n                            Modifier.liquefiable(liquidState)\n                        } else {\n                            Modifier\n                        },\n                    ),\n            colors =\n                CardDefaults.elevatedCardColors(\n                    containerColor = buttonColor,\n                ),\n            shape =\n                if (state.isObtainiumEnabled || isActiveDownload) {\n                    RoundedCornerShape(\n                        topStart = 24.dp,\n                        bottomStart = 24.dp,\n                        topEnd = 6.dp,\n                        bottomEnd = 6.dp,\n                    )\n                } else {\n                    CircleShape\n                },\n        ) {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center,\n            ) {\n                if (isActiveDownload) {\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.Center,\n                    ) {\n                        when (state.downloadStage) {\n                            DownloadStage.DOWNLOADING -> {\n                                Text(\n                                    text =\n                                        if (isUpdateAvailable) {\n                                            stringResource(Res.string.updating)\n                                        } else {\n                                            stringResource(\n                                                Res.string.downloading,\n                                            )\n                                        },\n                                    style = MaterialTheme.typography.titleMedium,\n                                    color = MaterialTheme.colorScheme.onPrimary,\n                                    fontWeight = FontWeight.Bold,\n                                )\n\n                                val progressText =\n                                    if (state.totalBytes != null && state.totalBytes > 0) {\n                                        \"${formatFileSize(state.downloadedBytes)} / ${\n                                            formatFileSize(\n                                                state.totalBytes,\n                                            )\n                                        }\"\n                                    } else {\n                                        \"${progress ?: 0}%\"\n                                    }\n                                Text(\n                                    text = progressText,\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f),\n                                )\n                            }\n\n                            DownloadStage.VERIFYING -> {\n                                Text(\n                                    text = stringResource(Res.string.verifying),\n                                    style = MaterialTheme.typography.titleMedium,\n                                    color = MaterialTheme.colorScheme.onPrimary,\n                                    fontWeight = FontWeight.Bold,\n                                )\n                            }\n\n                            DownloadStage.INSTALLING -> {\n                                Text(\n                                    text =\n                                        if (isUpdateAvailable) {\n                                            stringResource(Res.string.updating)\n                                        } else {\n                                            stringResource(\n                                                Res.string.installing,\n                                            )\n                                        },\n                                    style = MaterialTheme.typography.titleMedium,\n                                    color = MaterialTheme.colorScheme.onPrimary,\n                                    fontWeight = FontWeight.Bold,\n                                )\n                            }\n\n                            DownloadStage.IDLE -> {}\n                        }\n                    }\n                } else {\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.Center,\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(6.dp),\n                        ) {\n                            if (isUpdateAvailable) {\n                                Icon(\n                                    imageVector = Icons.Default.Update,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                    tint = MaterialTheme.colorScheme.onTertiary,\n                                )\n                            } else if (isInstalled) {\n                                Icon(\n                                    imageVector = Icons.Default.CheckCircle,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                    tint = MaterialTheme.colorScheme.onSecondary,\n                                )\n                            }\n\n                            Text(\n                                text = buttonText,\n                                color =\n                                    if (enabled) {\n                                        when {\n                                            isUpdateAvailable -> MaterialTheme.colorScheme.onTertiary\n                                            isInstalled -> MaterialTheme.colorScheme.onSecondary\n                                            else -> MaterialTheme.colorScheme.onPrimary\n                                        }\n                                    } else {\n                                        MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)\n                                    },\n                                fontWeight = FontWeight.Bold,\n                                style = MaterialTheme.typography.titleMedium,\n                            )\n                        }\n\n                        if (primaryAsset != null) {\n                            val assetArch = extractArchitectureFromName(primaryAsset.name)\n                            val systemArch = state.systemArchitecture\n                            val sizeText = formatFileSize(primaryAsset.size)\n                            val archLabel = assetArch ?: systemArch.name.lowercase()\n                            val subtitle = \"$archLabel  \\u2022  $sizeText\"\n\n                            Spacer(modifier = Modifier.height(2.dp))\n\n                            Row(\n                                horizontalArrangement = Arrangement.Center,\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                Text(\n                                    text = subtitle,\n                                    color =\n                                        if (enabled) {\n                                            when {\n                                                isUpdateAvailable -> {\n                                                    MaterialTheme.colorScheme.onTertiary.copy(\n                                                        alpha = 0.8f,\n                                                    )\n                                                }\n\n                                                isInstalled -> {\n                                                    MaterialTheme.colorScheme.onSecondary.copy(\n                                                        alpha = 0.8f,\n                                                    )\n                                                }\n\n                                                else -> {\n                                                    MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)\n                                                }\n                                            }\n                                        } else {\n                                            MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)\n                                        },\n                                    style = MaterialTheme.typography.bodySmall,\n                                )\n\n                                if (assetArch != null &&\n                                    isExactArchitectureMatch(\n                                        assetName = primaryAsset.name.lowercase(),\n                                        systemArch = systemArch,\n                                    )\n                                ) {\n                                    Spacer(modifier = Modifier.width(4.dp))\n\n                                    Icon(\n                                        imageVector = Icons.Default.CheckCircle,\n                                        contentDescription = stringResource(Res.string.architecture_compatible),\n                                        tint =\n                                            if (enabled) {\n                                                when {\n                                                    isUpdateAvailable -> {\n                                                        MaterialTheme.colorScheme.onTertiary.copy(\n                                                            alpha = 0.8f,\n                                                        )\n                                                    }\n\n                                                    isInstalled -> {\n                                                        MaterialTheme.colorScheme.onSecondary.copy(\n                                                            alpha = 0.8f,\n                                                        )\n                                                    }\n\n                                                    else -> {\n                                                        MaterialTheme.colorScheme.onPrimary.copy(\n                                                            alpha = 0.8f,\n                                                        )\n                                                    }\n                                                }\n                                            } else {\n                                                MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)\n                                            },\n                                        modifier = Modifier.size(14.dp),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        if (isActiveDownload) {\n            IconButton(\n                onClick = {\n                    onAction(DetailsAction.CancelCurrentDownload)\n                },\n                colors =\n                    IconButtonDefaults.iconButtonColors(\n                        containerColor = MaterialTheme.colorScheme.errorContainer,\n                    ),\n                modifier = Modifier.size(52.dp),\n                shape =\n                    RoundedCornerShape(\n                        topStart = 6.dp,\n                        bottomStart = 6.dp,\n                        topEnd = 24.dp,\n                        bottomEnd = 24.dp,\n                    ),\n            ) {\n                Icon(\n                    imageVector = Icons.Default.Close,\n                    contentDescription = stringResource(Res.string.cancel_download),\n                    modifier = Modifier.size(24.dp),\n                    tint = MaterialTheme.colorScheme.onErrorContainer,\n                )\n            }\n        } else if (state.isObtainiumEnabled) {\n            IconButton(\n                onClick = {\n                    onAction(DetailsAction.OnToggleInstallDropdown)\n                },\n                colors =\n                    IconButtonDefaults.iconButtonColors(\n                        containerColor =\n                            if (enabled) {\n                                buttonColor\n                            } else {\n                                MaterialTheme.colorScheme.surfaceContainer\n                            },\n                    ),\n                modifier = Modifier.size(52.dp),\n                shape =\n                    RoundedCornerShape(\n                        topStart = 6.dp,\n                        bottomStart = 6.dp,\n                        topEnd = 24.dp,\n                        bottomEnd = 24.dp,\n                    ),\n            ) {\n                Icon(\n                    imageVector = Icons.Default.KeyboardArrowDown,\n                    contentDescription = stringResource(Res.string.show_install_options),\n                    modifier = Modifier.size(24.dp),\n                    tint =\n                        if (enabled) {\n                            when {\n                                isUpdateAvailable -> MaterialTheme.colorScheme.onTertiary\n                                isInstalled -> MaterialTheme.colorScheme.onSecondary\n                                else -> MaterialTheme.colorScheme.onPrimary\n                            }\n                        } else {\n                            MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)\n                        },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AttestationBadge(attestationStatus: AttestationStatus) {\n    AnimatedVisibility(\n        visible = attestationStatus == AttestationStatus.VERIFIED || attestationStatus == AttestationStatus.CHECKING,\n        enter = fadeIn(),\n        exit = fadeOut(),\n    ) {\n        Row(\n            modifier = Modifier.padding(top = 8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.Center,\n        ) {\n            when (attestationStatus) {\n                AttestationStatus.CHECKING -> {\n                    CircularProgressIndicator(\n                        modifier = Modifier.size(14.dp),\n                        strokeWidth = 2.dp,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                    Spacer(modifier = Modifier.width(6.dp))\n                    Text(\n                        text = stringResource(Res.string.checking_attestation),\n                        style = MaterialTheme.typography.labelSmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n\n                AttestationStatus.VERIFIED -> {\n                    Icon(\n                        imageVector = Icons.Filled.VerifiedUser,\n                        contentDescription = null,\n                        modifier = Modifier.size(14.dp),\n                        tint = MaterialTheme.colorScheme.tertiary,\n                    )\n                    Spacer(modifier = Modifier.width(4.dp))\n                    Text(\n                        text = stringResource(Res.string.verified_build),\n                        style = MaterialTheme.typography.labelSmall,\n                        color = MaterialTheme.colorScheme.tertiary,\n                        fontWeight = FontWeight.SemiBold,\n                    )\n                }\n\n                else -> {}\n            }\n        }\n    }\n}\n\nprivate fun normalizeVersion(version: String): String = version.removePrefix(\"v\").removePrefix(\"V\").trim()\n\nprivate fun formatFileSize(bytes: Long): String =\n    when {\n        bytes >= 1_073_741_824 -> \"%.1f GB\".format(bytes / 1_073_741_824.0)\n        bytes >= 1_048_576 -> \"%.1f MB\".format(bytes / 1_048_576.0)\n        bytes >= 1_024 -> \"%.1f KB\".format(bytes / 1_024.0)\n        else -> \"$bytes B\"\n    }\n\n@Preview\n@Composable\nfun SmartInstallButtonDownloadingPreview() {\n    val liquidState = rememberLiquidState()\n    CompositionLocalProvider(LocalTopbarLiquidState provides liquidState) {\n        SmartInstallButton(\n            isDownloading = true,\n            isInstalling = false,\n            progress = 45,\n            primaryAsset =\n                GithubAsset(\n                    id = 1L,\n                    name = \"app-arm64-v8a.apk\",\n                    contentType = \"application/vnd.android.package-archive\",\n                    size = 50_000_000L,\n                    downloadUrl = \"https://example.com/app.apk\",\n                    uploader =\n                        GithubUser(\n                            id = 1L,\n                            login = \"developer\",\n                            avatarUrl = \"\",\n                            htmlUrl = \"\",\n                        ),\n                ),\n            onAction = {},\n            isLiquidGlassEnabled = true,\n            state =\n                DetailsState(\n                    isDownloading = true,\n                    downloadStage = DownloadStage.DOWNLOADING,\n                    downloadProgressPercent = 45,\n                ),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedCard\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun StatItem(\n    label: String,\n    stat: Int,\n    modifier: Modifier = Modifier,\n) {\n    OutlinedCard(\n        modifier = modifier,\n        colors =\n            CardDefaults.outlinedCardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,\n            ),\n    ) {\n        Column(\n            modifier = Modifier.padding(12.dp),\n        ) {\n            Text(\n                text = label,\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.outline,\n                maxLines = 1,\n                softWrap = false,\n            )\n\n            Text(\n                text = stat.toString(),\n                style = MaterialTheme.typography.titleLarge,\n                fontWeight = FontWeight.Black,\n                color = MaterialTheme.colorScheme.onBackground,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material.icons.filled.GTranslate\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.presentation.model.TranslationState\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun TranslationControls(\n    translationState: TranslationState,\n    onTranslateClick: () -> Unit,\n    onLanguagePickerClick: () -> Unit,\n    onToggleTranslation: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    AnimatedContent(\n        targetState = translationState.controlState,\n        modifier = modifier,\n        transitionSpec = {\n            (fadeIn() + slideInHorizontally { it / 3 }) togetherWith\n                (fadeOut() + slideOutHorizontally { -it / 3 })\n        },\n        label = \"translation_controls\",\n    ) { state ->\n        when (state) {\n            TranslationControlState.IDLE -> {\n                IdleControls(\n                    onTranslateClick = onTranslateClick,\n                    onLanguagePickerClick = onLanguagePickerClick,\n                )\n            }\n\n            TranslationControlState.TRANSLATING -> {\n                TranslatingIndicator()\n            }\n\n            TranslationControlState.SHOWING_TRANSLATION -> {\n                TranslatedControls(\n                    displayName = translationState.targetLanguageDisplayName,\n                    isShowingTranslation = true,\n                    onToggle = onToggleTranslation,\n                    onLanguagePickerClick = onLanguagePickerClick,\n                )\n            }\n\n            TranslationControlState.SHOWING_ORIGINAL -> {\n                TranslatedControls(\n                    displayName = translationState.targetLanguageDisplayName,\n                    isShowingTranslation = false,\n                    onToggle = onToggleTranslation,\n                    onLanguagePickerClick = onLanguagePickerClick,\n                )\n            }\n\n            TranslationControlState.ERROR -> {\n                ErrorControls(\n                    onRetry = onTranslateClick,\n                    onLanguagePickerClick = onLanguagePickerClick,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun IdleControls(\n    onTranslateClick: () -> Unit,\n    onLanguagePickerClick: () -> Unit,\n) {\n    Row(\n        modifier =\n            Modifier\n                .clip(RoundedCornerShape(20.dp))\n                .background(MaterialTheme.colorScheme.surfaceContainerHigh)\n                .clickable(onClick = onTranslateClick)\n                .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = Icons.Default.GTranslate,\n            contentDescription = stringResource(Res.string.translate),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n            modifier = Modifier.size(16.dp),\n        )\n        Spacer(Modifier.width(5.dp))\n        Text(\n            text = stringResource(Res.string.translate),\n            style = MaterialTheme.typography.labelMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n        LanguageDropdownButton(onLanguagePickerClick)\n    }\n}\n\n@Composable\nprivate fun TranslatingIndicator() {\n    Row(\n        modifier =\n            Modifier\n                .clip(RoundedCornerShape(20.dp))\n                .background(MaterialTheme.colorScheme.primaryContainer)\n                .padding(horizontal = 12.dp, vertical = 6.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(6.dp),\n    ) {\n        CircularProgressIndicator(\n            modifier = Modifier.size(14.dp),\n            strokeWidth = 2.dp,\n            color = MaterialTheme.colorScheme.onPrimaryContainer,\n        )\n        Text(\n            text = stringResource(Res.string.translating),\n            style = MaterialTheme.typography.labelMedium,\n            color = MaterialTheme.colorScheme.onPrimaryContainer,\n        )\n    }\n}\n\n@Composable\nprivate fun TranslatedControls(\n    displayName: String?,\n    isShowingTranslation: Boolean,\n    onToggle: () -> Unit,\n    onLanguagePickerClick: () -> Unit,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Row(\n            modifier =\n                Modifier\n                    .clip(RoundedCornerShape(20.dp))\n                    .background(\n                        if (isShowingTranslation) {\n                            MaterialTheme.colorScheme.primaryContainer\n                        } else {\n                            MaterialTheme.colorScheme.surfaceContainerHigh\n                        },\n                    ).clickable(onClick = onToggle)\n                    .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            AnimatedVisibility(\n                visible = isShowingTranslation,\n                enter = fadeIn() + scaleIn(),\n                exit = fadeOut() + scaleOut(),\n            ) {\n                Row {\n                    Icon(\n                        imageVector = Icons.Default.GTranslate,\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                        modifier = Modifier.size(14.dp),\n                    )\n                    Spacer(Modifier.width(4.dp))\n                }\n            }\n\n            Text(\n                text =\n                    if (isShowingTranslation) {\n                        stringResource(Res.string.show_original)\n                    } else {\n                        displayName ?: stringResource(Res.string.translate)\n                    },\n                style = MaterialTheme.typography.labelMedium,\n                fontWeight = FontWeight.Medium,\n                color =\n                    if (isShowingTranslation) {\n                        MaterialTheme.colorScheme.onPrimaryContainer\n                    } else {\n                        MaterialTheme.colorScheme.onSurfaceVariant\n                    },\n            )\n\n            LanguageDropdownButton(\n                onClick = onLanguagePickerClick,\n                tint =\n                    if (isShowingTranslation) {\n                        MaterialTheme.colorScheme.onPrimaryContainer\n                    } else {\n                        MaterialTheme.colorScheme.onSurfaceVariant\n                    },\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ErrorControls(\n    onRetry: () -> Unit,\n    onLanguagePickerClick: () -> Unit,\n) {\n    Row(\n        modifier =\n            Modifier\n                .clip(RoundedCornerShape(20.dp))\n                .background(MaterialTheme.colorScheme.errorContainer)\n                .clickable(onClick = onRetry)\n                .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = Icons.Default.Refresh,\n            contentDescription = stringResource(Res.string.translation_error_retry),\n            tint = MaterialTheme.colorScheme.onErrorContainer,\n            modifier = Modifier.size(14.dp),\n        )\n        Spacer(Modifier.width(4.dp))\n        Text(\n            text = stringResource(Res.string.translation_error_retry),\n            style = MaterialTheme.typography.labelMedium,\n            color = MaterialTheme.colorScheme.onErrorContainer,\n        )\n        LanguageDropdownButton(\n            onClick = onLanguagePickerClick,\n            tint = MaterialTheme.colorScheme.onErrorContainer,\n        )\n    }\n}\n\n@Composable\nprivate fun LanguageDropdownButton(\n    onClick: () -> Unit,\n    tint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant,\n) {\n    Icon(\n        imageVector = Icons.Default.ArrowDropDown,\n        contentDescription = stringResource(Res.string.change_language),\n        tint = tint,\n        modifier =\n            Modifier\n                .size(24.dp)\n                .clip(RoundedCornerShape(12.dp))\n                .clickable(onClick = onClick)\n                .padding(2.dp),\n    )\n}\n\nprivate enum class TranslationControlState {\n    IDLE,\n    TRANSLATING,\n    SHOWING_TRANSLATION,\n    SHOWING_ORIGINAL,\n    ERROR,\n}\n\nprivate val TranslationState.controlState: TranslationControlState\n    get() =\n        when {\n            isTranslating -> TranslationControlState.TRANSLATING\n            error != null && translatedText == null -> TranslationControlState.ERROR\n            isShowingTranslation && translatedText != null -> TranslationControlState.SHOWING_TRANSLATION\n            !isShowingTranslation && translatedText != null -> TranslationControlState.SHOWING_ORIGINAL\n            else -> TranslationControlState.IDLE\n        }\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.CheckCircle\nimport androidx.compose.material.icons.filled.UnfoldMore\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedCard\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.latest_badge\nimport zed.rainxch.githubstore.core.presentation.res.no_version_selected\nimport zed.rainxch.githubstore.core.presentation.res.not_available\nimport zed.rainxch.githubstore.core.presentation.res.pre_release_badge\nimport zed.rainxch.githubstore.core.presentation.res.select_version\nimport zed.rainxch.githubstore.core.presentation.res.versions_title\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun VersionPicker(\n    selectedRelease: GithubRelease?,\n    filteredReleases: List<GithubRelease>,\n    isPickerVisible: Boolean,\n    onAction: (DetailsAction) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    val isPickerEnabled by remember(filteredReleases) {\n        derivedStateOf { filteredReleases.isNotEmpty() }\n    }\n\n    Column(\n        modifier = modifier.wrapContentHeight(),\n        verticalArrangement = Arrangement.spacedBy(4.dp),\n    ) {\n        Text(\n            text = stringResource(Res.string.versions_title),\n            style = MaterialTheme.typography.labelLargeEmphasized,\n            color = MaterialTheme.colorScheme.tertiary,\n            modifier = Modifier.padding(horizontal = 4.dp),\n        )\n        OutlinedCard(\n            onClick = { onAction(DetailsAction.ToggleVersionPicker) },\n            enabled = isPickerEnabled,\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 12.dp)\n                        .heightIn(min = 36.dp),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text =\n                            selectedRelease?.tagName\n                                ?: stringResource(Res.string.no_version_selected),\n                        style = MaterialTheme.typography.titleSmall,\n                        fontWeight = FontWeight.SemiBold,\n                        overflow = TextOverflow.Clip,\n                        maxLines = 1,\n                    )\n                    selectedRelease?.name?.let { name ->\n                        if (name != selectedRelease.tagName) {\n                            Text(\n                                text = name,\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        }\n                    }\n                }\n                Icon(\n                    imageVector = Icons.Default.UnfoldMore,\n                    contentDescription = stringResource(Res.string.select_version),\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n\n    if (isPickerVisible) {\n        val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)\n\n        ModalBottomSheet(\n            onDismissRequest = { onAction(DetailsAction.ToggleVersionPicker) },\n            sheetState = sheetState,\n        ) {\n            Column(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .navigationBarsPadding(),\n            ) {\n                Text(\n                    text = stringResource(Res.string.versions_title),\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.Bold,\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                )\n\n                HorizontalDivider()\n\n                if (filteredReleases.isEmpty()) {\n                    Text(\n                        text = stringResource(Res.string.not_available),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        modifier = Modifier.padding(16.dp),\n                    )\n                } else {\n                    val latestReleaseId by remember(filteredReleases) {\n                        derivedStateOf { filteredReleases.firstOrNull()?.id }\n                    }\n\n                    LazyColumn(\n                        modifier = Modifier.fillMaxWidth(),\n                        contentPadding = PaddingValues(vertical = 8.dp),\n                    ) {\n                        items(\n                            items = filteredReleases,\n                            key = { it.id },\n                        ) { release ->\n                            VersionListItem(\n                                release = release,\n                                isSelected = release.id == selectedRelease?.id,\n                                isLatest = release.id == latestReleaseId,\n                                onClick = { onAction(DetailsAction.SelectRelease(release)) },\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun VersionListItem(\n    release: GithubRelease,\n    isSelected: Boolean,\n    isLatest: Boolean,\n    onClick: () -> Unit,\n) {\n    Row(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .clickable(\n                    onClickLabel = stringResource(Res.string.select_version),\n                    onClick = onClick,\n                ).padding(horizontal = 16.dp, vertical = 12.dp),\n        horizontalArrangement = Arrangement.SpaceBetween,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(modifier = Modifier.weight(1f)) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                Text(\n                    text = release.tagName,\n                    style = MaterialTheme.typography.titleSmall,\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    color =\n                        if (isSelected) {\n                            MaterialTheme.colorScheme.primary\n                        } else {\n                            MaterialTheme.colorScheme.onSurface\n                        },\n                )\n                if (isLatest) {\n                    Surface(\n                        shape = RoundedCornerShape(4.dp),\n                        color = MaterialTheme.colorScheme.primaryContainer,\n                    ) {\n                        Text(\n                            text = stringResource(Res.string.latest_badge),\n                            style = MaterialTheme.typography.labelSmall,\n                            modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),\n                            color = MaterialTheme.colorScheme.onPrimaryContainer,\n                        )\n                    }\n                }\n                if (release.isPrerelease) {\n                    Surface(\n                        shape = RoundedCornerShape(4.dp),\n                        color = MaterialTheme.colorScheme.tertiaryContainer,\n                    ) {\n                        Text(\n                            text = stringResource(Res.string.pre_release_badge),\n                            style = MaterialTheme.typography.labelSmall,\n                            modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),\n                            color = MaterialTheme.colorScheme.onTertiaryContainer,\n                        )\n                    }\n                }\n            }\n\n            release.name?.let { name ->\n                if (name != release.tagName) {\n                    Text(\n                        text = name,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                }\n            }\n\n            Text(\n                text = release.publishedAt.take(10),\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.outline,\n            )\n        }\n\n        if (isSelected) {\n            Spacer(Modifier.width(8.dp))\n            Icon(\n                imageVector = Icons.Default.CheckCircle,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.size(20.dp),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionTypePicker.kt",
    "content": "package zed.rainxch.details.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.domain.model.ReleaseCategory\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.category_all\nimport zed.rainxch.githubstore.core.presentation.res.category_pre_release\nimport zed.rainxch.githubstore.core.presentation.res.category_stable\n\n@Composable\nfun VersionTypePicker(\n    selectedCategory: ReleaseCategory,\n    onAction: (DetailsAction) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    LazyRow(\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n        modifier = modifier.fillMaxWidth(),\n    ) {\n        items(ReleaseCategory.entries) { category ->\n            FilterChip(\n                selected = category == selectedCategory,\n                onClick = { onAction(DetailsAction.SelectReleaseCategory(category)) },\n                label = {\n                    Text(\n                        text =\n                            when (category) {\n                                ReleaseCategory.STABLE -> stringResource(Res.string.category_stable)\n                                ReleaseCategory.PRE_RELEASE -> stringResource(Res.string.category_pre_release)\n                                ReleaseCategory.ALL -> stringResource(Res.string.category_all)\n                            },\n                    )\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.mikepenz.markdown.compose.Markdown\nimport com.mikepenz.markdown.model.ImageTransformer\nimport io.github.fletchmckee.liquid.liquefiable\nimport org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.presentation.components.TranslationControls\nimport zed.rainxch.details.presentation.model.TranslationState\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.details.presentation.utils.MarkdownImageTransformer\nimport zed.rainxch.details.presentation.utils.rememberMarkdownColors\nimport zed.rainxch.details.presentation.utils.rememberMarkdownTypography\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nfun LazyListScope.about(\n    readmeMarkdown: String,\n    readmeLanguage: String?,\n    isExpanded: Boolean,\n    isLiquidGlassEnabled: Boolean,\n    onToggleExpanded: () -> Unit,\n    collapsedHeight: Dp,\n    translationState: TranslationState,\n    onTranslateClick: () -> Unit,\n    onLanguagePickerClick: () -> Unit,\n    onToggleTranslation: () -> Unit,\n) {\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n\n        Spacer(Modifier.height(16.dp))\n\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(bottom = 8.dp),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                Text(\n                    text = stringResource(Res.string.about_this_app),\n                    style = MaterialTheme.typography.titleLarge,\n                    color = MaterialTheme.colorScheme.onBackground,\n                    fontWeight = FontWeight.Bold,\n                    modifier = Modifier.then(\n                        if (isLiquidGlassEnabled) {\n                            Modifier.liquefiable(liquidState)\n                        } else {\n                            Modifier\n                        },\n                    ),\n                )\n\n                readmeLanguage?.let {\n                    Text(\n                        text = it,\n                        style = MaterialTheme.typography.labelSmall,\n                        color = MaterialTheme.colorScheme.outline,\n                        modifier = Modifier.then(\n                            if (isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n                    )\n                }\n            }\n\n            TranslationControls(\n                translationState = translationState,\n                onTranslateClick = onTranslateClick,\n                onLanguagePickerClick = onLanguagePickerClick,\n                onToggleTranslation = onToggleTranslation,\n            )\n        }\n    }\n\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        val displayContent =\n            if (translationState.isShowingTranslation && translationState.translatedText != null) {\n                translationState.translatedText\n            } else {\n                readmeMarkdown\n            }\n\n        AnimatedContent(\n            targetState = displayContent,\n            transitionSpec = { fadeIn() togetherWith fadeOut() },\n            label = \"about_content\",\n        ) { content ->\n            ExpandableMarkdownContent(\n                content = content,\n                isExpanded = isExpanded,\n                onToggleExpanded = onToggleExpanded,\n                imageTransformer = MarkdownImageTransformer,\n                collapsedHeight = collapsedHeight,\n                fadeColor = MaterialTheme.colorScheme.background,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .then(\n                            if (isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        )\n                        .animateContentSize(),\n            )\n        }\n    }\n}\n\n@Composable\nfun ExpandableMarkdownContent(\n    content: String,\n    isExpanded: Boolean,\n    onToggleExpanded: () -> Unit,\n    imageTransformer: ImageTransformer,\n    collapsedHeight: Dp,\n    fadeColor: Color,\n    modifier: Modifier = Modifier,\n) {\n    val density = LocalDensity.current\n    val colors = rememberMarkdownColors()\n    val typography = rememberMarkdownTypography()\n    val flavour = remember { GFMFlavourDescriptor() }\n\n    val collapsedHeightPx = with(density) { collapsedHeight.toPx() }\n    var contentHeightPx by remember(content, collapsedHeightPx) { mutableStateOf(0f) }\n    val needsExpansion = contentHeightPx > collapsedHeightPx && collapsedHeightPx > 0f\n\n    Column(\n        modifier = modifier.animateContentSize(),\n    ) {\n        Box {\n            Surface(\n                color = Color.Transparent,\n                contentColor = MaterialTheme.colorScheme.onBackground,\n                modifier =\n                    if (!isExpanded && needsExpansion) {\n                        Modifier.heightIn(max = collapsedHeight).clipToBounds()\n                    } else {\n                        Modifier\n                    },\n            ) {\n                Markdown(\n                    content = content,\n                    colors = colors,\n                    typography = typography,\n                    flavour = flavour,\n                    imageTransformer = imageTransformer,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .onGloballyPositioned { coordinates ->\n                                val measured = coordinates.size.height.toFloat()\n                                if (measured > contentHeightPx) {\n                                    contentHeightPx = measured\n                                }\n                            },\n                )\n            }\n\n            if (!isExpanded && needsExpansion) {\n                Box(\n                    modifier =\n                        Modifier\n                            .align(Alignment.BottomCenter)\n                            .fillMaxWidth()\n                            .height(80.dp)\n                            .background(\n                                Brush.verticalGradient(\n                                    0f to fadeColor.copy(alpha = 0f),\n                                    1f to fadeColor,\n                                ),\n                            ),\n                )\n            }\n        }\n\n        if (needsExpansion) {\n            TextButton(\n                onClick = onToggleExpanded,\n                modifier = Modifier.align(Alignment.CenterHorizontally),\n            ) {\n                Text(\n                    text =\n                        if (isExpanded) {\n                            stringResource(Res.string.show_less)\n                        } else {\n                            stringResource(Res.string.read_more)\n                        },\n                    style = MaterialTheme.typography.labelLarge,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.OpenInNew\nimport androidx.compose.material.icons.filled.Security\nimport androidx.compose.material.icons.filled.Update\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport io.github.fletchmckee.liquid.liquefiable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.details.presentation.DetailsState\nimport zed.rainxch.details.presentation.components.AppHeader\nimport zed.rainxch.details.presentation.components.ReleaseAssetsPicker\nimport zed.rainxch.details.presentation.components.SmartInstallButton\nimport zed.rainxch.details.presentation.components.VersionPicker\nimport zed.rainxch.details.presentation.components.VersionTypePicker\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.appmanager_description\nimport zed.rainxch.githubstore.core.presentation.res.external_installer_description\nimport zed.rainxch.githubstore.core.presentation.res.inspect_with_appmanager\nimport zed.rainxch.githubstore.core.presentation.res.obtainium_description\nimport zed.rainxch.githubstore.core.presentation.res.open_in_obtainium\nimport zed.rainxch.githubstore.core.presentation.res.open_with_external_installer\n\nfun LazyListScope.header(\n    state: DetailsState,\n    onAction: (DetailsAction) -> Unit,\n) {\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        if (state.repository != null) {\n            AppHeader(\n                author = state.userProfile,\n                release = state.selectedRelease,\n                repository = state.repository,\n                installedApp = state.installedApp,\n                downloadStage = state.downloadStage,\n                downloadProgress = state.downloadProgressPercent,\n                modifier =\n                    Modifier.then(\n                        if (state.isLiquidGlassEnabled) {\n                            Modifier.liquefiable(liquidState)\n                        } else {\n                            Modifier\n                        },\n                    ),\n            )\n        }\n    }\n\n    // versions type list\n    if (state.allReleases.isNotEmpty()) {\n        item {\n            VersionTypePicker(\n                selectedCategory = state.selectedReleaseCategory,\n                onAction = onAction,\n                modifier = Modifier.fillMaxWidth().animateItem(),\n            )\n        }\n    }\n\n    // version and installable release\n    if (state.allReleases.isNotEmpty() || state.installableAssets.isNotEmpty()) {\n        item {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(6.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                ReleaseAssetsPicker(\n                    assetsList = state.installableAssets,\n                    selectedAsset = state.primaryAsset,\n                    isPickerVisible = state.isReleaseSelectorVisible,\n                    onAction = onAction,\n                    modifier = Modifier.weight(.65f),\n                )\n                VersionPicker(\n                    selectedRelease = state.selectedRelease,\n                    filteredReleases = state.filteredReleases,\n                    isPickerVisible = state.isVersionPickerVisible,\n                    onAction = onAction,\n                    modifier = Modifier.weight(.35f),\n                )\n            }\n        }\n    }\n\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        Box(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            SmartInstallButton(\n                isDownloading = state.isDownloading,\n                isInstalling = state.isInstalling,\n                isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n                progress = state.downloadProgressPercent,\n                primaryAsset = state.primaryAsset,\n                state = state,\n                onAction = onAction,\n            )\n\n            DropdownMenu(\n                expanded = state.isInstallDropdownExpanded,\n                onDismissRequest = {\n                    onAction(DetailsAction.OnToggleInstallDropdown)\n                },\n                offset = DpOffset(x = 0.dp, y = 20.dp),\n            ) {\n                DropdownMenuItem(\n                    text = {\n                        Column {\n                            Text(\n                                text = stringResource(Res.string.open_in_obtainium),\n                                style = MaterialTheme.typography.bodyLarge,\n                                color = MaterialTheme.colorScheme.onSurface,\n                            )\n                            Text(\n                                text = stringResource(Res.string.obtainium_description),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    },\n                    onClick = {\n                        onAction(DetailsAction.OpenInObtainium)\n                    },\n                    leadingIcon = {\n                        Icon(\n                            imageVector = Icons.Default.Update,\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    },\n                    modifier =\n                        Modifier.then(\n                            if (state.isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n                )\n\n                Spacer(Modifier.height(8.dp))\n\n                DropdownMenuItem(\n                    text = {\n                        Column {\n                            Text(\n                                text = stringResource(Res.string.inspect_with_appmanager),\n                                style = MaterialTheme.typography.bodyLarge,\n                                color = MaterialTheme.colorScheme.onSurface,\n                            )\n                            Text(\n                                text = stringResource(Res.string.appmanager_description),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    },\n                    onClick = {\n                        onAction(DetailsAction.OpenInAppManager)\n                    },\n                    leadingIcon = {\n                        Icon(\n                            imageVector = Icons.Default.Security,\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    },\n                    modifier =\n                        Modifier.then(\n                            if (state.isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n                )\n\n                Spacer(Modifier.height(8.dp))\n\n                DropdownMenuItem(\n                    text = {\n                        Column {\n                            Text(\n                                text = stringResource(Res.string.open_with_external_installer),\n                                style = MaterialTheme.typography.bodyLarge,\n                                color = MaterialTheme.colorScheme.onSurface,\n                            )\n                            Text(\n                                text = stringResource(Res.string.external_installer_description),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    },\n                    onClick = {\n                        onAction(DetailsAction.InstallWithExternalApp)\n                    },\n                    leadingIcon = {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.OpenInNew,\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    },\n                    modifier =\n                        Modifier.then(\n                            if (state.isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport io.github.fletchmckee.liquid.liquefiable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.presentation.DetailsState\nimport zed.rainxch.details.presentation.model.LogResult\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.details.presentation.utils.asText\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nfun LazyListScope.logs(state: DetailsState) {\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        HorizontalDivider()\n\n        Text(\n            text = stringResource(Res.string.install_logs),\n            style = MaterialTheme.typography.titleLarge,\n            color = MaterialTheme.colorScheme.onBackground,\n            modifier =\n                Modifier\n                    .padding(vertical = 8.dp)\n                    .then(\n                        if (state.isLiquidGlassEnabled) {\n                            Modifier.liquefiable(liquidState)\n                        } else {\n                            Modifier\n                        },\n                    ),\n            fontWeight = FontWeight.Bold,\n        )\n    }\n\n    items(state.installLogs) { log ->\n        val liquidState = LocalTopbarLiquidState.current\n\n        Text(\n            text = \"> ${log.result.asText()}: ${log.assetName}\",\n            style =\n                MaterialTheme.typography.labelSmall.copy(\n                    fontStyle = FontStyle.Italic,\n                ),\n            color =\n                if (log.result is LogResult.Error) {\n                    MaterialTheme.colorScheme.error\n                } else {\n                    MaterialTheme.colorScheme.outline\n                },\n            modifier =\n                Modifier.then(\n                    if (state.isLiquidGlassEnabled) {\n                        Modifier.liquefiable(liquidState)\n                    } else {\n                        Modifier\n                    },\n                ),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedCard\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.coil3.CoilImage\nimport io.github.fletchmckee.liquid.liquefiable\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.GithubUserProfile\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.author(\n    isLiquidGlassEnabled: Boolean,\n    author: GithubUserProfile?,\n    onAction: (DetailsAction) -> Unit,\n) {\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n\n        Spacer(Modifier.height(16.dp))\n\n        Text(\n            text = stringResource(Res.string.author),\n            style = MaterialTheme.typography.titleLarge,\n            color = MaterialTheme.colorScheme.onBackground,\n            modifier =\n                Modifier\n                    .padding(bottom = 12.dp)\n                    .then(\n                        if (isLiquidGlassEnabled) {\n                            Modifier.liquefiable(liquidState)\n                        } else {\n                            Modifier\n                        },\n                    ),\n            fontWeight = FontWeight.Bold,\n        )\n\n        OutlinedCard(\n            onClick = {\n                author?.login?.let { author ->\n                    onAction(\n                        DetailsAction.OpenDeveloperProfile(\n                            author,\n                        ),\n                    )\n                }\n            },\n            colors =\n                CardDefaults.outlinedCardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,\n                ),\n            shape = RoundedCornerShape(32.dp),\n        ) {\n            Row(\n                modifier = Modifier.padding(12.dp),\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CoilImage(\n                    imageModel = { author?.avatarUrl },\n                    modifier =\n                        Modifier\n                            .size(80.dp)\n                            .clip(CircleShape)\n                            .then(\n                                if (isLiquidGlassEnabled) {\n                                    Modifier.liquefiable(liquidState)\n                                } else {\n                                    Modifier\n                                },\n                            ),\n                    loading = {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            CircularWavyProgressIndicator()\n                        }\n                    },\n                )\n\n                Column(\n                    modifier = Modifier.weight(1f),\n                    verticalArrangement = Arrangement.spacedBy(4.dp),\n                ) {\n                    author?.login?.let {\n                        Text(\n                            text = it,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurface,\n                            modifier =\n                                Modifier.then(\n                                    if (isLiquidGlassEnabled) {\n                                        Modifier.liquefiable(liquidState)\n                                    } else {\n                                        Modifier\n                                    },\n                                ),\n                        )\n                    }\n\n                    author?.bio?.let { bio ->\n                        Text(\n                            text = bio,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.outline,\n                            maxLines = 2,\n                            softWrap = false,\n                            overflow = TextOverflow.Ellipsis,\n                            modifier =\n                                Modifier.then(\n                                    if (isLiquidGlassEnabled) {\n                                        Modifier.liquefiable(liquidState)\n                                    } else {\n                                        Modifier\n                                    },\n                                ),\n                        )\n                    }\n\n                    Spacer(Modifier.height(4.dp))\n\n                    author?.htmlUrl?.let {\n                        Row(\n                            modifier =\n                                Modifier.clickable {\n                                    onAction(DetailsAction.OpenAuthorInBrowser)\n                                },\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        ) {\n                            Icon(\n                                painter = painterResource(Res.drawable.ic_github),\n                                contentDescription = null,\n                                modifier = Modifier.size(24.dp),\n                                tint = MaterialTheme.colorScheme.primary,\n                            )\n\n                            Text(\n                                text = stringResource(Res.string.profile),\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.primary,\n                            )\n                        }\n                    }\n                }\n\n                author?.login?.let { author ->\n                    IconButton(\n                        onClick = {\n                            onAction(DetailsAction.OpenDeveloperProfile(author))\n                        },\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                            contentDescription = stringResource(Res.string.open_developer_profile),\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReportIssue.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.BugReport\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedCard\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.open_github_link\nimport zed.rainxch.githubstore.core.presentation.res.open_in_browser\nimport zed.rainxch.githubstore.core.presentation.res.report_issue\n\nfun LazyListScope.reportIssue(repoUrl: String) {\n    item {\n        val uriHandler = LocalUriHandler.current\n\n        OutlinedCard(\n            onClick = {\n                uriHandler.openUri(\"${repoUrl.trimEnd('/')}/issues\")\n            },\n            colors =\n                CardDefaults.outlinedCardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,\n                ),\n            shape = RoundedCornerShape(32.dp),\n        ) {\n            Row(\n                modifier = Modifier.padding(12.dp),\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Icon(\n                    imageVector = Icons.Default.BugReport,\n                    contentDescription = stringResource(Res.string.report_issue),\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                    modifier = Modifier.size(32.dp),\n                )\n\n                Text(\n                    text = stringResource(Res.string.report_issue),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                    modifier = Modifier.weight(1f),\n                )\n\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                    contentDescription = stringResource(Res.string.open_in_browser),\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                    modifier = Modifier.size(20.dp),\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport io.github.fletchmckee.liquid.liquefiable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.domain.model.RepoStats\nimport zed.rainxch.details.presentation.components.StatItem\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nfun LazyListScope.stats(\n    isLiquidGlassEnabled: Boolean,\n    repoStats: RepoStats,\n) {\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        Spacer(Modifier.height(16.dp))\n\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp),\n        ) {\n            StatItem(\n                label = stringResource(Res.string.forks),\n                stat = repoStats.forks,\n                modifier =\n                    Modifier\n                        .weight(1.5f)\n                        .then(\n                            if (isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n            )\n\n            StatItem(\n                label = stringResource(Res.string.stars),\n                stat = repoStats.stars,\n                modifier =\n                    Modifier\n                        .weight(2f)\n                        .then(\n                            if (isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n            )\n\n            StatItem(\n                label = stringResource(Res.string.issues),\n                stat = repoStats.openIssues,\n                modifier =\n                    Modifier\n                        .weight(1f)\n                        .then(\n                            if (isLiquidGlassEnabled) {\n                                Modifier.liquefiable(liquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt",
    "content": "package zed.rainxch.details.presentation.components.sections\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.mikepenz.markdown.compose.Markdown\nimport io.github.fletchmckee.liquid.LiquidState\nimport io.github.fletchmckee.liquid.liquefiable\nimport org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.GithubRelease\nimport zed.rainxch.details.presentation.components.TranslationControls\nimport zed.rainxch.details.presentation.model.TranslationState\nimport zed.rainxch.details.presentation.utils.LocalTopbarLiquidState\nimport zed.rainxch.details.presentation.utils.MarkdownImageTransformer\nimport zed.rainxch.details.presentation.utils.rememberMarkdownColors\nimport zed.rainxch.details.presentation.utils.rememberMarkdownTypography\nimport zed.rainxch.githubstore.core.presentation.res.*\n\nfun LazyListScope.whatsNew(\n    release: GithubRelease,\n    isExpanded: Boolean,\n    isLiquidGlassEnabled: Boolean,\n    onToggleExpanded: () -> Unit,\n    collapsedHeight: Dp,\n    translationState: TranslationState,\n    onTranslateClick: () -> Unit,\n    onLanguagePickerClick: () -> Unit,\n    onToggleTranslation: () -> Unit,\n) {\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n\n        Spacer(Modifier.height(16.dp))\n\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(bottom = 8.dp),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Text(\n                text = stringResource(Res.string.whats_new),\n                style = MaterialTheme.typography.titleLarge,\n                color = MaterialTheme.colorScheme.onBackground,\n                modifier =\n                    Modifier.then(\n                        if (isLiquidGlassEnabled) {\n                            Modifier.liquefiable(liquidState)\n                        } else {\n                            Modifier\n                        },\n                    ),\n                fontWeight = FontWeight.Bold,\n            )\n\n            TranslationControls(\n                translationState = translationState,\n                onTranslateClick = onTranslateClick,\n                onLanguagePickerClick = onLanguagePickerClick,\n                onToggleTranslation = onToggleTranslation,\n            )\n        }\n\n        Spacer(Modifier.height(8.dp))\n\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n            colors =\n                CardDefaults.cardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                ),\n        ) {\n            Column(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(16.dp),\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        release.tagName,\n                        style = MaterialTheme.typography.labelLarge,\n                        color = MaterialTheme.colorScheme.primary,\n                        modifier =\n                            Modifier.then(\n                                if (isLiquidGlassEnabled) {\n                                    Modifier.liquefiable(liquidState)\n                                } else {\n                                    Modifier\n                                },\n                            ),\n                    )\n\n                    Text(\n                        release.publishedAt.take(10),\n                        style = MaterialTheme.typography.labelMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        modifier =\n                            Modifier.then(\n                                if (isLiquidGlassEnabled) {\n                                    Modifier.liquefiable(liquidState)\n                                } else {\n                                    Modifier\n                                },\n                            ),\n                    )\n                }\n            }\n        }\n    }\n\n    item {\n        val liquidState = LocalTopbarLiquidState.current\n\n        Spacer(Modifier.height(12.dp))\n\n        ExpandableMarkdownContent(\n            translationState = translationState,\n            release = release,\n            collapsedHeight = collapsedHeight,\n            isExpanded = isExpanded,\n            isLiquidGlassEnabled = isLiquidGlassEnabled,\n            liquidState = liquidState,\n            onToggleExpanded = onToggleExpanded,\n        )\n    }\n}\n\n@Composable\nprivate fun ExpandableMarkdownContent(\n    translationState: TranslationState,\n    release: GithubRelease,\n    collapsedHeight: Dp,\n    isExpanded: Boolean,\n    isLiquidGlassEnabled: Boolean,\n    liquidState: LiquidState,\n    onToggleExpanded: () -> Unit,\n) {\n    val displayContent =\n        if (translationState.isShowingTranslation && translationState.translatedText != null) {\n            translationState.translatedText\n        } else {\n            release.description ?: stringResource(Res.string.no_release_notes)\n        }\n\n    val density = LocalDensity.current\n    val colors = rememberMarkdownColors()\n    val typography = rememberMarkdownTypography()\n    val flavour = remember { GFMFlavourDescriptor() }\n    val cardColor = MaterialTheme.colorScheme.surfaceContainerLow\n\n    AnimatedContent(\n        targetState = displayContent,\n        transitionSpec = { fadeIn() togetherWith fadeOut() },\n        label = \"whats_new_content\",\n    ) { content ->\n\n        val collapsedHeightPx = with(density) { collapsedHeight.toPx() }\n        var contentHeightPx by remember(content, collapsedHeightPx) {\n            mutableFloatStateOf(0f)\n        }\n        val needsExpansion =\n            remember(contentHeightPx, collapsedHeightPx) {\n                contentHeightPx > collapsedHeightPx && collapsedHeightPx > 0f\n            }\n\n        Column(\n            modifier = Modifier.animateContentSize(),\n        ) {\n            Box {\n                Box(\n                    modifier =\n                        if (!isExpanded && needsExpansion) {\n                            Modifier.heightIn(max = collapsedHeight).clipToBounds()\n                        } else {\n                            Modifier\n                        },\n                ) {\n                    Markdown(\n                        content = content,\n                        colors = colors,\n                        typography = typography,\n                        flavour = flavour,\n                        imageTransformer = MarkdownImageTransformer,\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .then(\n                                    if (isLiquidGlassEnabled) {\n                                        Modifier.liquefiable(liquidState)\n                                    } else {\n                                        Modifier\n                                    },\n                                )\n                                .onGloballyPositioned { coordinates ->\n                                    val measured = coordinates.size.height.toFloat()\n                                    if (measured > contentHeightPx) {\n                                        contentHeightPx = measured\n                                    }\n                                },\n                    )\n                }\n\n                if (!isExpanded && needsExpansion) {\n                    Box(\n                        modifier =\n                            Modifier\n                                .align(Alignment.BottomCenter)\n                                .fillMaxWidth()\n                                .height(80.dp)\n                                .background(\n                                    Brush.verticalGradient(\n                                        0f to cardColor.copy(alpha = 0f),\n                                        1f to cardColor,\n                                    ),\n                                ),\n                    )\n                }\n            }\n\n            if (needsExpansion) {\n                TextButton(\n                    onClick = onToggleExpanded,\n                    modifier = Modifier.align(Alignment.CenterHorizontally),\n                ) {\n                    Text(\n                        text =\n                            if (isExpanded) {\n                                stringResource(Res.string.show_less)\n                            } else {\n                                stringResource(Res.string.read_more)\n                            },\n                        style = MaterialTheme.typography.labelLarge,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/states/ErrorState.kt",
    "content": "package zed.rainxch.details.presentation.components.states\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.details.presentation.DetailsAction\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun ErrorState(\n    errorMessage: String,\n    onAction: (DetailsAction) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxSize(),\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Text(\n            text = stringResource(Res.string.error_loading_details),\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.onBackground,\n            textAlign = TextAlign.Center,\n        )\n\n        Text(\n            text = errorMessage,\n            style = MaterialTheme.typography.titleSmall,\n            color = MaterialTheme.colorScheme.error,\n        )\n\n        GithubStoreButton(\n            text = stringResource(Res.string.retry),\n            onClick = {\n                onAction(DetailsAction.Retry)\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/AttestationStatus.kt",
    "content": "package zed.rainxch.details.presentation.model\n\nenum class AttestationStatus {\n    UNCHECKED,\n    CHECKING,\n    VERIFIED,\n    UNVERIFIED,\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/DownloadStage.kt",
    "content": "package zed.rainxch.details.presentation.model\n\nenum class DownloadStage {\n    IDLE,\n    DOWNLOADING,\n    VERIFYING,\n    INSTALLING,\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/InstallLogItem.kt",
    "content": "package zed.rainxch.details.presentation.model\n\ndata class InstallLogItem(\n    val timeIso: String,\n    val assetName: String,\n    val assetSizeBytes: Long,\n    val releaseTag: String,\n    val result: LogResult,\n)\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/LogResult.kt",
    "content": "package zed.rainxch.details.presentation.model\n\nsealed class LogResult {\n    data object DownloadStarted : LogResult()\n\n    data object UpdateStarted : LogResult()\n\n    data object Downloaded : LogResult()\n\n    data object InstallStarted : LogResult()\n\n    data object Installed : LogResult()\n\n    data object Updated : LogResult()\n\n    data object Cancelled : LogResult()\n\n    data object PreparingForAppManager : LogResult()\n\n    data object OpenedInAppManager : LogResult()\n\n    data object PermissionBlocked : LogResult()\n\n    data object OpenedInExternalInstaller : LogResult()\n\n    data class Error(\n        val message: String?,\n    ) : LogResult()\n\n    data class Info(\n        val message: String,\n    ) : LogResult()\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/ShowDowngradeWarning.kt",
    "content": "package zed.rainxch.details.presentation.model\n\ndata class DowngradeWarning(\n    val packageName: String,\n    val currentVersion: String,\n    val targetVersion: String,\n)\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/SigningKeyWarning.kt",
    "content": "package zed.rainxch.details.presentation.model\n\ndata class SigningKeyWarning(\n    val packageName: String,\n    val expectedFingerprint: String,\n    val actualFingerprint: String,\n    val pendingDownloadUrl: String,\n    val pendingAssetName: String,\n    val pendingSizeBytes: Long,\n    val pendingReleaseTag: String,\n    val pendingIsUpdate: Boolean,\n    val pendingFilePath: String,\n)\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/SupportedLanguages.kt",
    "content": "package zed.rainxch.details.presentation.model\n\nimport zed.rainxch.details.domain.model.SupportedLanguage\n\nobject SupportedLanguages {\n    val all: List<SupportedLanguage> =\n        listOf(\n            SupportedLanguage(\"ar\", \"Arabic\"),\n            SupportedLanguage(\"bn\", \"Bengali\"),\n            SupportedLanguage(\"zh-CN\", \"Chinese (Simplified)\"),\n            SupportedLanguage(\"zh-TW\", \"Chinese (Traditional)\"),\n            SupportedLanguage(\"cs\", \"Czech\"),\n            SupportedLanguage(\"da\", \"Danish\"),\n            SupportedLanguage(\"nl\", \"Dutch\"),\n            SupportedLanguage(\"en\", \"English\"),\n            SupportedLanguage(\"fi\", \"Finnish\"),\n            SupportedLanguage(\"fr\", \"French\"),\n            SupportedLanguage(\"de\", \"German\"),\n            SupportedLanguage(\"el\", \"Greek\"),\n            SupportedLanguage(\"he\", \"Hebrew\"),\n            SupportedLanguage(\"hi\", \"Hindi\"),\n            SupportedLanguage(\"hu\", \"Hungarian\"),\n            SupportedLanguage(\"id\", \"Indonesian\"),\n            SupportedLanguage(\"it\", \"Italian\"),\n            SupportedLanguage(\"ja\", \"Japanese\"),\n            SupportedLanguage(\"ko\", \"Korean\"),\n            SupportedLanguage(\"ms\", \"Malay\"),\n            SupportedLanguage(\"no\", \"Norwegian\"),\n            SupportedLanguage(\"pl\", \"Polish\"),\n            SupportedLanguage(\"pt\", \"Portuguese\"),\n            SupportedLanguage(\"pt-BR\", \"Portuguese (Brazil)\"),\n            SupportedLanguage(\"ro\", \"Romanian\"),\n            SupportedLanguage(\"ru\", \"Russian\"),\n            SupportedLanguage(\"es\", \"Spanish\"),\n            SupportedLanguage(\"sv\", \"Swedish\"),\n            SupportedLanguage(\"th\", \"Thai\"),\n            SupportedLanguage(\"tr\", \"Turkish\"),\n            SupportedLanguage(\"uk\", \"Ukrainian\"),\n            SupportedLanguage(\"uz\", \"Uzbek\"),\n            SupportedLanguage(\"vi\", \"Vietnamese\"),\n        )\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/TranslationState.kt",
    "content": "package zed.rainxch.details.presentation.model\n\ndata class TranslationState(\n    val isTranslating: Boolean = false,\n    val translatedText: String? = null,\n    val isShowingTranslation: Boolean = false,\n    val targetLanguageCode: String? = null,\n    val targetLanguageDisplayName: String? = null,\n    val detectedSourceLanguage: String? = null,\n    val error: String? = null,\n)\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/TranslationTarget.kt",
    "content": "package zed.rainxch.details.presentation.model\n\nsealed interface TranslationTarget {\n    data object About : TranslationTarget\n\n    data object WhatsNew : TranslationTarget\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/LocalTopbarLiquidState.kt",
    "content": "package zed.rainxch.details.presentation.utils\n\nimport androidx.compose.runtime.compositionLocalOf\nimport io.github.fletchmckee.liquid.LiquidState\n\ninternal val LocalTopbarLiquidState =\n    compositionLocalOf<LiquidState> {\n        error(\"State not declared\")\n    }\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/LogResultAsText.kt",
    "content": "package zed.rainxch.details.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.details.presentation.model.LogResult\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun LogResult.asText(): String =\n    when (this) {\n        LogResult.DownloadStarted -> {\n            stringResource(Res.string.log_download_started)\n        }\n\n        LogResult.Downloaded -> {\n            stringResource(Res.string.log_downloaded)\n        }\n\n        LogResult.InstallStarted -> {\n            stringResource(Res.string.log_install_started)\n        }\n\n        LogResult.Installed -> {\n            stringResource(Res.string.log_installed)\n        }\n\n        LogResult.Updated -> {\n            stringResource(Res.string.log_updated)\n        }\n\n        LogResult.Cancelled -> {\n            stringResource(Res.string.log_cancelled)\n        }\n\n        LogResult.OpenedInAppManager -> {\n            stringResource(Res.string.log_opened_appmanager)\n        }\n\n        is LogResult.Error -> {\n            message?.let {\n                stringResource(Res.string.log_error_with_message, it)\n            } ?: stringResource(Res.string.log_error)\n        }\n\n        is LogResult.Info -> {\n            message\n        }\n\n        LogResult.PreparingForAppManager -> {\n            stringResource(Res.string.log_prepare_appmanager)\n        }\n\n        LogResult.UpdateStarted -> {\n            stringResource(Res.string.log_update_started)\n        }\n\n        LogResult.PermissionBlocked -> {\n            stringResource(Res.string.log_permission_blocked)\n        }\n\n        LogResult.OpenedInExternalInstaller -> {\n            stringResource(Res.string.log_opened_external_installer)\n        }\n    }\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt",
    "content": "package zed.rainxch.details.presentation.utils\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.layout.ContentScale\nimport coil3.compose.rememberAsyncImagePainter\nimport com.mikepenz.markdown.model.ImageData\nimport com.mikepenz.markdown.model.ImageTransformer\n\nobject MarkdownImageTransformer : ImageTransformer {\n    @Composable\n    override fun transform(link: String): ImageData? {\n        if (link.isBlank()) {\n            return null\n        }\n\n        val normalizedLink =\n            if (link.contains(\"github.com\") && link.contains(\"/blob/\")) {\n                link\n                    .replace(\"github.com\", \"raw.githubusercontent.com\")\n                    .replace(\"/blob/\", \"/\")\n            } else {\n                link\n            }\n\n        if (normalizedLink.endsWith(\".svg\", ignoreCase = true) ||\n            normalizedLink.contains(\".svg?\", ignoreCase = true) ||\n            normalizedLink.contains(\".svg#\", ignoreCase = true)\n        ) {\n            return null\n        }\n\n        if (!normalizedLink.startsWith(\"http://\") &&\n            !normalizedLink.startsWith(\"https://\") &&\n            !normalizedLink.startsWith(\"data:\")\n        ) {\n            return null\n        }\n\n        val painter =\n            rememberAsyncImagePainter(\n                model = normalizedLink,\n            )\n\n        return ImageData(\n            painter = painter,\n            modifier = Modifier.fillMaxWidth(),\n            contentDescription = \"Image\",\n            contentScale = ContentScale.Fit,\n        )\n    }\n\n    @Composable\n    override fun intrinsicSize(painter: Painter): Size = painter.intrinsicSize\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownUtils.kt",
    "content": "package zed.rainxch.details.presentation.utils\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextDecoration\nimport com.mikepenz.markdown.model.DefaultMarkdownColors\nimport com.mikepenz.markdown.model.DefaultMarkdownTypography\nimport com.mikepenz.markdown.model.MarkdownColors\nimport com.mikepenz.markdown.model.MarkdownTypography\n\n@Composable\nfun rememberMarkdownColors(): MarkdownColors {\n    val colorScheme = MaterialTheme.colorScheme\n\n    return DefaultMarkdownColors(\n        text = colorScheme.onBackground,\n        codeBackground = colorScheme.surfaceVariant.copy(alpha = 0.5f),\n        inlineCodeBackground = colorScheme.surfaceVariant.copy(alpha = 0.5f),\n        dividerColor = colorScheme.outlineVariant,\n        tableBackground = colorScheme.surface,\n    )\n}\n\n@Composable\nfun rememberMarkdownTypography(): MarkdownTypography {\n    val typography = MaterialTheme.typography\n    val colorScheme = MaterialTheme.colorScheme\n\n    return DefaultMarkdownTypography(\n        h1 = typography.headlineMedium.copy(fontWeight = FontWeight.Bold),\n        h2 = typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),\n        h3 = typography.titleMedium.copy(fontWeight = FontWeight.Medium),\n        h4 = typography.titleSmall.copy(fontWeight = FontWeight.Medium),\n        h5 = typography.titleSmall.copy(fontWeight = FontWeight.Normal),\n        h6 = typography.labelLarge.copy(fontWeight = FontWeight.Bold),\n        text = typography.bodyLarge,\n        code =\n            typography.bodyMedium.copy(\n                fontFamily = FontFamily.Monospace,\n                color = colorScheme.onSurfaceVariant,\n            ),\n        inlineCode =\n            typography.bodyMedium.copy(\n                fontFamily = FontFamily.Monospace,\n                color = colorScheme.onSurfaceVariant,\n            ),\n        quote =\n            typography.bodyLarge.copy(\n                fontStyle = FontStyle.Italic,\n                color = colorScheme.onSurfaceVariant,\n            ),\n        paragraph = typography.bodyLarge,\n        ordered = typography.bodyLarge,\n        bullet = typography.bodyLarge,\n        list = typography.bodyLarge,\n        textLink =\n            TextLinkStyles(\n                style =\n                    SpanStyle(\n                        color = colorScheme.primary,\n                        textDecoration = TextDecoration.Underline,\n                    ),\n            ),\n        table = typography.bodyMedium,\n    )\n}\n"
  },
  {
    "path": "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/SystemArchitecture.kt",
    "content": "package zed.rainxch.details.presentation.utils\n\nimport zed.rainxch.core.domain.model.AssetArchitectureMatcher\nimport zed.rainxch.core.domain.model.SystemArchitecture\n\nfun extractArchitectureFromName(name: String): String? =\n    when (AssetArchitectureMatcher.detectArchitecture(name)) {\n        SystemArchitecture.X86_64 -> \"x86_64\"\n        SystemArchitecture.AARCH64 -> \"aarch64\"\n        SystemArchitecture.X86 -> \"i386\"\n        SystemArchitecture.ARM -> \"arm\"\n        SystemArchitecture.UNKNOWN, null -> null\n    }\n\nfun isExactArchitectureMatch(\n    assetName: String,\n    systemArch: SystemArchitecture,\n): Boolean = AssetArchitectureMatcher.isExactMatch(assetName, systemArch)\n"
  },
  {
    "path": "feature/dev-profile/CLAUDE.md",
    "content": "# CLAUDE.md - Developer Profile Feature\n\n## Purpose\n\nDisplays a GitHub developer/user profile. Shows user info (avatar, bio, stats), their repositories with filtering and sorting, and follower/following counts. Reached by clicking on a developer's name from any repository card.\n\n## Module Structure\n\n```\nfeature/dev-profile/\n├── domain/\n│   ├── model/\n│   │   ├── DeveloperProfile.kt       # User profile data model\n│   │   ├── DeveloperRepository.kt    # User's repository model\n│   │   ├── RepoFilterType.kt         # Filter: All, Sources, Forks, etc.\n│   │   └── RepoSortType.kt           # Sort: Stars, Name, Updated, etc.\n│   └── repository/DeveloperProfileRepository.kt  # Profile + repos\n├── data/\n│   ├── di/SharedModule.kt            # Koin: devProfileModule\n│   ├── repository/DeveloperProfileRepositoryImpl.kt\n│   ├── dto/                           # Network DTOs\n│   └── mappers/                       # DTO → domain model mappers\n└── presentation/\n    ├── DeveloperProfileViewModel.kt   # Profile loading, repo filtering/sorting\n    ├── DeveloperProfileState.kt       # profile, repos, filters, loading\n    ├── DeveloperProfileAction.kt      # Load, filter, sort, click actions\n    ├── DeveloperProfileEvent.kt       # One-off events\n    ├── DeveloperProfileRoot.kt        # Main composable\n    └── components/                    # Profile header, repo list, filter controls\n```\n\n## Key Interfaces\n\n```kotlin\ninterface DeveloperProfileRepository {\n    suspend fun getDeveloperProfile(username: String): Result<DeveloperProfile>\n    suspend fun getDeveloperRepositories(username: String): Result<List<DeveloperRepository>>\n}\n```\n\n## Navigation\n\nRoute: `GithubStoreGraph.DeveloperProfileScreen(username: String)`\n\n## Implementation Notes\n\n- Profile and repos are fetched in parallel on load\n- Client-side filtering by `RepoFilterType` (All, Sources, Forks) and sorting by `RepoSortType` (Stars, Name, Updated)\n- Both API calls return `Result<T>` for error handling\n- Reached from repository cards throughout the app (home, search, details, favourites, starred)\n"
  },
  {
    "path": "feature/dev-profile/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/dev-profile/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.devProfile.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/di/SharedModule.kt",
    "content": "package zed.rainxch.devprofile.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.devprofile.data.repository.DeveloperProfileRepositoryImpl\nimport zed.rainxch.devprofile.domain.repository.DeveloperProfileRepository\n\nval devProfileModule =\n    module {\n        single<DeveloperProfileRepository> {\n            DeveloperProfileRepositoryImpl(\n                logger = get(),\n                httpClient = get(),\n                platform = get(),\n                installedAppsDao = get(),\n                favouritesRepository = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubRepoResponse.kt",
    "content": "package zed.rainxch.devprofile.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GitHubRepoResponse(\n    val id: Long,\n    val name: String,\n    @SerialName(\"full_name\") val fullName: String,\n    val description: String? = null,\n    @SerialName(\"html_url\") val htmlUrl: String,\n    @SerialName(\"stargazers_count\") val stargazersCount: Int,\n    @SerialName(\"forks_count\") val forksCount: Int,\n    @SerialName(\"open_issues_count\") val openIssuesCount: Int,\n    val language: String? = null,\n    @SerialName(\"updated_at\") val updatedAt: String,\n    @SerialName(\"pushed_at\") val pushedAt: String? = null,\n    @SerialName(\"has_downloads\") val hasDownloads: Boolean = false,\n    val archived: Boolean = false,\n    val fork: Boolean = false,\n)\n"
  },
  {
    "path": "feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubUserResponse.kt",
    "content": "package zed.rainxch.devprofile.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GitHubUserResponse(\n    val login: String,\n    val name: String? = null,\n    @SerialName(\"avatar_url\") val avatarUrl: String,\n    val bio: String? = null,\n    val company: String? = null,\n    val location: String? = null,\n    val email: String? = null,\n    val blog: String? = null,\n    @SerialName(\"twitter_username\") val twitterUsername: String? = null,\n    @SerialName(\"public_repos\") val publicRepos: Int,\n    @SerialName(\"public_gists\") val publicGists: Int,\n    val followers: Int,\n    val following: Int,\n    @SerialName(\"created_at\") val createdAt: String,\n    @SerialName(\"updated_at\") val updatedAt: String,\n    @SerialName(\"html_url\") val htmlUrl: String,\n)\n"
  },
  {
    "path": "feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt",
    "content": "package zed.rainxch.devprofile.data.mappers\n\nimport zed.rainxch.devprofile.data.dto.GitHubRepoResponse\nimport zed.rainxch.devprofile.domain.model.DeveloperRepository\n\nfun GitHubRepoResponse.toDomain(\n    hasReleases: Boolean = false,\n    hasInstallableAssets: Boolean = false,\n    isInstalled: Boolean = false,\n    isFavorite: Boolean = false,\n    latestVersion: String? = null,\n) = DeveloperRepository(\n    id = id,\n    name = name,\n    fullName = fullName,\n    description = description,\n    htmlUrl = htmlUrl,\n    stargazersCount = stargazersCount,\n    forksCount = forksCount,\n    openIssuesCount = openIssuesCount,\n    language = language,\n    hasReleases = hasReleases,\n    hasInstallableAssets = hasInstallableAssets,\n    isInstalled = isInstalled,\n    isFavorite = isFavorite,\n    latestVersion = latestVersion,\n    updatedAt = updatedAt,\n    pushedAt = pushedAt,\n)\n"
  },
  {
    "path": "feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubUserToDomain.kt",
    "content": "package zed.rainxch.devprofile.data.mappers\n\nimport zed.rainxch.devprofile.data.dto.GitHubUserResponse\nimport zed.rainxch.devprofile.domain.model.DeveloperProfile\n\nfun GitHubUserResponse.toDomain() =\n    DeveloperProfile(\n        login = login,\n        name = name,\n        avatarUrl = avatarUrl,\n        bio = bio,\n        company = company,\n        location = location,\n        email = email,\n        blog = blog,\n        twitterUsername = twitterUsername,\n        publicRepos = publicRepos,\n        publicGists = publicGists,\n        followers = followers,\n        following = following,\n        createdAt = createdAt,\n        updatedAt = updatedAt,\n        htmlUrl = htmlUrl,\n    )\n"
  },
  {
    "path": "feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt",
    "content": "package zed.rainxch.devprofile.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.http.isSuccess\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withPermit\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport zed.rainxch.core.data.local.db.dao.InstalledAppDao\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.devprofile.data.dto.GitHubRepoResponse\nimport zed.rainxch.devprofile.data.dto.GitHubUserResponse\nimport zed.rainxch.devprofile.data.mappers.toDomain\nimport zed.rainxch.devprofile.domain.model.DeveloperProfile\nimport zed.rainxch.devprofile.domain.model.DeveloperRepository\nimport zed.rainxch.devprofile.domain.repository.DeveloperProfileRepository\n\nclass DeveloperProfileRepositoryImpl(\n    private val httpClient: HttpClient,\n    private val platform: Platform,\n    private val installedAppsDao: InstalledAppDao,\n    private val favouritesRepository: FavouritesRepository,\n    private val logger: GitHubStoreLogger,\n) : DeveloperProfileRepository {\n    override suspend fun getDeveloperProfile(username: String): Result<DeveloperProfile> {\n        return withContext(Dispatchers.IO) {\n            try {\n                val response = httpClient.get(\"/users/$username\")\n\n                if (!response.status.isSuccess()) {\n                    return@withContext Result.failure(\n                        Exception(\"Failed to fetch developer profile: ${response.status.description}\"),\n                    )\n                }\n\n                val userResponse: GitHubUserResponse = response.body()\n                Result.success(userResponse.toDomain())\n            } catch (e: RateLimitException) {\n                throw e\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                logger.error(\"Failed to fetch developer profile for $username\")\n                Result.failure(e)\n            }\n        }\n    }\n\n    override suspend fun getDeveloperRepositories(username: String): Result<List<DeveloperRepository>> {\n        return withContext(Dispatchers.IO) {\n            try {\n                val allRepos = mutableListOf<GitHubRepoResponse>()\n                var page = 1\n                val perPage = 100\n\n                while (true) {\n                    val response =\n                        httpClient.get(\"/users/$username/repos\") {\n                            parameter(\"per_page\", perPage)\n                            parameter(\"page\", page)\n                            parameter(\"type\", \"owner\")\n                            parameter(\"sort\", \"updated\")\n                            parameter(\"direction\", \"desc\")\n                        }\n\n                    if (!response.status.isSuccess()) {\n                        return@withContext Result.failure(\n                            Exception(\"Failed to fetch repositories: ${response.status.description}\"),\n                        )\n                    }\n\n                    val repos: List<GitHubRepoResponse> = response.body()\n\n                    if (repos.isEmpty()) break\n\n                    allRepos.addAll(repos.filter { !it.archived && !it.fork })\n\n                    if (repos.size < perPage) break\n                    page++\n                }\n\n                val allFavorites = favouritesRepository.getAllFavorites().first()\n                val favoriteIds = allFavorites.map { it.repoId }.toSet()\n\n                val processedRepos =\n                    coroutineScope {\n                        val semaphore = Semaphore(20)\n                        val deferredResults =\n                            allRepos.map { repo ->\n                                async {\n                                    semaphore.withPermit {\n                                        processRepository(repo, favoriteIds)\n                                    }\n                                }\n                            }\n                        deferredResults.awaitAll()\n                    }\n\n                Result.success(processedRepos)\n            } catch (e: RateLimitException) {\n                throw e\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                logger.error(\"Failed to fetch repositories for $username\")\n                Result.failure(e)\n            }\n        }\n    }\n\n    private suspend fun processRepository(\n        repo: GitHubRepoResponse,\n        favoriteIds: Set<Long>,\n    ): DeveloperRepository {\n        val installedApp = installedAppsDao.getAppByRepoId(repo.id)\n        val isFavorite = favoriteIds.contains(repo.id)\n\n        val (hasReleases, hasInstallableAssets, latestVersion) =\n            checkReleaseInfo(\n                owner = repo.fullName.split(\"/\")[0],\n                repoName = repo.name,\n            )\n\n        return repo.toDomain(\n            hasReleases = hasReleases,\n            hasInstallableAssets = hasInstallableAssets,\n            isInstalled = installedApp != null,\n            isFavorite = isFavorite,\n            latestVersion = latestVersion,\n        )\n    }\n\n    private suspend fun checkReleaseInfo(\n        owner: String,\n        repoName: String,\n    ): Triple<Boolean, Boolean, String?> {\n        return try {\n            val response =\n                httpClient.get(\"/repos/$owner/$repoName/releases\") {\n                    parameter(\"per_page\", 10)\n                }\n\n            if (!response.status.isSuccess()) {\n                return Triple(false, false, null)\n            }\n\n            val releases: List<ReleaseNetworkModel> = response.body()\n\n            val stableRelease =\n                releases.firstOrNull {\n                    it.draft != true && it.prerelease != true\n                }\n\n            if (stableRelease == null) {\n                return Triple(releases.isNotEmpty(), false, null)\n            }\n\n            val hasInstallableAssets =\n                stableRelease.assets.any { asset ->\n                    val name = asset.name.lowercase()\n                    when (platform) {\n                        Platform.ANDROID -> {\n                            name.endsWith(\".apk\")\n                        }\n\n                        Platform.WINDOWS -> {\n                            name.endsWith(\".msi\") || name.endsWith(\".exe\")\n                        }\n\n                        Platform.MACOS -> {\n                            name.endsWith(\".dmg\") || name.endsWith(\".pkg\")\n                        }\n\n                        Platform.LINUX -> {\n                            name.endsWith(\".appimage\") || name.endsWith(\".deb\") ||\n                                name.endsWith(\".rpm\")\n                        }\n                    }\n                }\n\n            Triple(\n                true,\n                hasInstallableAssets,\n                if (hasInstallableAssets) stableRelease.tagName else null,\n            )\n        } catch (e: RateLimitException) {\n            throw e\n        } catch (e: CancellationException) {\n            throw e\n        } catch (e: Exception) {\n            logger.warn(\"Failed to check releases for $owner/$repoName : ${e.message}\")\n            Triple(false, false, null)\n        }\n    }\n\n    @Serializable\n    private data class ReleaseNetworkModel(\n        val assets: List<AssetNetworkModel>,\n        val draft: Boolean? = null,\n        val prerelease: Boolean? = null,\n        @SerialName(\"tag_name\") val tagName: String,\n        @SerialName(\"published_at\") val publishedAt: String? = null,\n    )\n\n    @Serializable\n    private data class AssetNetworkModel(\n        val name: String,\n    )\n}\n"
  },
  {
    "path": "feature/dev-profile/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/dev-profile/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperProfile.kt",
    "content": "package zed.rainxch.devprofile.domain.model\n\ndata class DeveloperProfile(\n    val login: String,\n    val name: String?,\n    val avatarUrl: String,\n    val bio: String?,\n    val company: String?,\n    val location: String?,\n    val email: String?,\n    val blog: String?,\n    val twitterUsername: String?,\n    val publicRepos: Int,\n    val publicGists: Int,\n    val followers: Int,\n    val following: Int,\n    val createdAt: String,\n    val updatedAt: String,\n    val htmlUrl: String,\n)\n"
  },
  {
    "path": "feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt",
    "content": "package zed.rainxch.devprofile.domain.model\n\ndata class DeveloperRepository(\n    val id: Long,\n    val name: String,\n    val fullName: String,\n    val description: String?,\n    val htmlUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val openIssuesCount: Int,\n    val language: String?,\n    val hasReleases: Boolean,\n    val hasInstallableAssets: Boolean,\n    val isInstalled: Boolean = false,\n    val isFavorite: Boolean = false,\n    val latestVersion: String? = null,\n    val updatedAt: String,\n    val pushedAt: String?,\n)\n"
  },
  {
    "path": "feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/RepoFilterType.kt",
    "content": "package zed.rainxch.devprofile.domain.model\n\nenum class RepoFilterType {\n    WITH_RELEASES,\n    INSTALLED,\n    FAVORITES,\n}\n"
  },
  {
    "path": "feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/RepoSortType.kt",
    "content": "package zed.rainxch.devprofile.domain.model\n\nenum class RepoSortType {\n    UPDATED,\n    STARS,\n    NAME,\n}\n"
  },
  {
    "path": "feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/repository/DeveloperProfileRepository.kt",
    "content": "package zed.rainxch.devprofile.domain.repository\n\nimport zed.rainxch.devprofile.domain.model.DeveloperProfile\nimport zed.rainxch.devprofile.domain.model.DeveloperRepository\n\ninterface DeveloperProfileRepository {\n    suspend fun getDeveloperProfile(username: String): Result<DeveloperProfile>\n\n    suspend fun getDeveloperRepositories(username: String): Result<List<DeveloperRepository>>\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/dev-profile/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.devProfile.domain)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n\n                implementation(libs.bundles.landscapist)\n                implementation(libs.kotlinx.collections.immutable)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileAction.kt",
    "content": "package zed.rainxch.devprofile.presentation\n\nimport zed.rainxch.devprofile.domain.model.DeveloperRepository\nimport zed.rainxch.devprofile.domain.model.RepoFilterType\nimport zed.rainxch.devprofile.domain.model.RepoSortType\n\nsealed interface DeveloperProfileAction {\n    data object OnNavigateBackClick : DeveloperProfileAction\n\n    data class OnRepositoryClick(\n        val repoId: Long,\n    ) : DeveloperProfileAction\n\n    data class OnFilterChange(\n        val filter: RepoFilterType,\n    ) : DeveloperProfileAction\n\n    data class OnSortChange(\n        val sort: RepoSortType,\n    ) : DeveloperProfileAction\n\n    data class OnSearchQueryChange(\n        val query: String,\n    ) : DeveloperProfileAction\n\n    data class OnToggleFavorite(\n        val repository: DeveloperRepository,\n    ) : DeveloperProfileAction\n\n    data object OnDismissError : DeveloperProfileAction\n\n    data object OnRetry : DeveloperProfileAction\n\n    data class OnOpenLink(\n        val url: String,\n    ) : DeveloperProfileAction\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt",
    "content": "package zed.rainxch.devprofile.presentation\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Error\nimport androidx.compose.material.icons.filled.FolderOff\nimport androidx.compose.material.icons.filled.OpenInBrowser\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Snackbar\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.devprofile.domain.model.RepoFilterType\nimport zed.rainxch.devprofile.presentation.components.DeveloperRepoItem\nimport zed.rainxch.devprofile.presentation.components.FilterSortControls\nimport zed.rainxch.devprofile.presentation.components.ProfileInfoCard\nimport zed.rainxch.devprofile.presentation.components.StatsRow\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun DeveloperProfileRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToDetails: (repoId: Long) -> Unit,\n    viewModel: DeveloperProfileViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    val uriHandler = LocalUriHandler.current\n\n    DeveloperProfileScreen(\n        state = state,\n        onAction = { action ->\n            when (action) {\n                DeveloperProfileAction.OnNavigateBackClick -> {\n                    onNavigateBack()\n                }\n\n                is DeveloperProfileAction.OnRepositoryClick -> {\n                    onNavigateToDetails(action.repoId)\n                }\n\n                is DeveloperProfileAction.OnOpenLink -> {\n                    val url = action.url.trim()\n                    val allowed = url.startsWith(\"https://\") || url.startsWith(\"http://\")\n                    if (allowed) uriHandler.openUri(url)\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun DeveloperProfileScreen(\n    state: DeveloperProfileState,\n    onAction: (DeveloperProfileAction) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            DevProfileTopbar(\n                state = state,\n                onAction = onAction,\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.background,\n    ) { innerPadding ->\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n        ) {\n            when {\n                state.isLoading -> {\n                    CircularWavyProgressIndicator(\n                        modifier = Modifier.align(Alignment.Center),\n                    )\n                }\n\n                state.errorMessage != null && state.profile == null -> {\n                    ErrorContent(\n                        message = state.errorMessage,\n                        onRetry = { onAction(DeveloperProfileAction.OnRetry) },\n                        modifier = Modifier.align(Alignment.Center),\n                    )\n                }\n\n                state.profile != null -> {\n                    LazyColumn(\n                        modifier = Modifier.fillMaxSize(),\n                        contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),\n                        verticalArrangement = Arrangement.spacedBy(12.dp),\n                    ) {\n                        item {\n                            ProfileInfoCard(\n                                profile = state.profile,\n                                onAction = onAction,\n                            )\n                        }\n\n                        item {\n                            StatsRow(profile = state.profile)\n                        }\n\n                        item {\n                            FilterSortControls(\n                                currentFilter = state.currentFilter,\n                                currentSort = state.currentSort,\n                                searchQuery = state.searchQuery,\n                                repoCount = state.filteredRepositories.size,\n                                totalCount = state.repositories.size,\n                                onAction = onAction,\n                            )\n                        }\n\n                        if (state.isLoadingRepos) {\n                            item {\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .padding(32.dp),\n                                    contentAlignment = Alignment.Center,\n                                ) {\n                                    CircularWavyProgressIndicator()\n                                }\n                            }\n                        } else if (state.filteredRepositories.isEmpty()) {\n                            item {\n                                EmptyReposContent(\n                                    filter = state.currentFilter,\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .padding(32.dp),\n                                )\n                            }\n                        } else {\n                            items(\n                                items = state.filteredRepositories,\n                                key = { it.id },\n                            ) { repo ->\n                                DeveloperRepoItem(\n                                    repository = repo,\n                                    onItemClick = {\n                                        onAction(DeveloperProfileAction.OnRepositoryClick(repo.id))\n                                    },\n                                    onToggleFavorite = {\n                                        onAction(DeveloperProfileAction.OnToggleFavorite(repo))\n                                    },\n                                    modifier = Modifier.animateItem(),\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n\n            if (state.errorMessage != null && state.profile != null) {\n                Snackbar(\n                    modifier =\n                        Modifier\n                            .align(Alignment.BottomCenter)\n                            .padding(16.dp),\n                    action = {\n                        TextButton(\n                            onClick = {\n                                onAction(DeveloperProfileAction.OnRetry)\n                            },\n                        ) {\n                            Text(\n                                text = stringResource(Res.string.retry),\n                            )\n                        }\n                    },\n                    dismissAction = {\n                        IconButton(\n                            onClick = {\n                                onAction(DeveloperProfileAction.OnDismissError)\n                            },\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.Close,\n                                contentDescription = stringResource(Res.string.dismiss),\n                            )\n                        }\n                    },\n                ) {\n                    Text(\n                        text = state.errorMessage,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EmptyReposContent(\n    filter: RepoFilterType,\n    modifier: Modifier = Modifier,\n) {\n    val message =\n        when (filter) {\n            RepoFilterType.WITH_RELEASES -> stringResource(Res.string.no_repos_with_releases)\n            RepoFilterType.INSTALLED -> stringResource(Res.string.no_installed_repos)\n            RepoFilterType.FAVORITES -> stringResource(Res.string.no_favorite_repos)\n        }\n\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        Icon(\n            imageVector = Icons.Default.FolderOff,\n            contentDescription = null,\n            modifier = Modifier.size(48.dp),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        Text(\n            text = message,\n            maxLines = 2,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun DevProfileTopbar(\n    state: DeveloperProfileState,\n    onAction: (DeveloperProfileAction) -> Unit,\n) {\n    TopAppBar(\n        navigationIcon = {\n            IconButton(\n                shapes = IconButtonDefaults.shapes(),\n                onClick = { onAction(DeveloperProfileAction.OnNavigateBackClick) },\n            ) {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                    contentDescription = stringResource(Res.string.navigate_back),\n                    modifier = Modifier.size(24.dp),\n                )\n            }\n        },\n        title = {\n            Text(\n                text = state.username,\n                style = MaterialTheme.typography.titleMediumEmphasized,\n                fontWeight = FontWeight.SemiBold,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n        },\n        actions = {\n            state.profile?.htmlUrl?.let {\n                IconButton(\n                    shapes = IconButtonDefaults.shapes(),\n                    onClick = {\n                        onAction(DeveloperProfileAction.OnOpenLink(it))\n                    },\n                    colors =\n                        IconButtonDefaults.iconButtonColors(\n                            contentColor = MaterialTheme.colorScheme.onSurface,\n                        ),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.OpenInBrowser,\n                        contentDescription = stringResource(Res.string.open_repository),\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n        },\n    )\n}\n\n@Composable\nprivate fun ErrorContent(\n    message: String,\n    onRetry: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Column(\n        modifier = modifier.padding(32.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        Icon(\n            imageVector = Icons.Default.Error,\n            contentDescription = null,\n            modifier = Modifier.size(64.dp),\n            tint = MaterialTheme.colorScheme.error,\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Text(\n            text = stringResource(Res.string.error_generic, message),\n            maxLines = 3,\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.onSurface,\n            textAlign = TextAlign.Center,\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        GithubStoreButton(\n            text = stringResource(Res.string.retry),\n            onClick = {\n                onRetry()\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileState.kt",
    "content": "package zed.rainxch.devprofile.presentation\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport zed.rainxch.devprofile.domain.model.DeveloperProfile\nimport zed.rainxch.devprofile.domain.model.DeveloperRepository\nimport zed.rainxch.devprofile.domain.model.RepoFilterType\nimport zed.rainxch.devprofile.domain.model.RepoSortType\n\ndata class DeveloperProfileState(\n    val username: String = \"\",\n    val profile: DeveloperProfile? = null,\n    val repositories: ImmutableList<DeveloperRepository> = persistentListOf(),\n    val filteredRepositories: ImmutableList<DeveloperRepository> = persistentListOf(),\n    val isLoading: Boolean = false,\n    val isLoadingRepos: Boolean = false,\n    val errorMessage: String? = null,\n    val currentFilter: RepoFilterType = RepoFilterType.WITH_RELEASES,\n    val currentSort: RepoSortType = RepoSortType.UPDATED,\n    val searchQuery: String = \"\",\n)\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage zed.rainxch.devprofile.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.collections.immutable.toImmutableList\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.core.domain.model.FavoriteRepo\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.devprofile.domain.model.RepoFilterType\nimport zed.rainxch.devprofile.domain.model.RepoSortType\nimport zed.rainxch.devprofile.domain.repository.DeveloperProfileRepository\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport kotlin.coroutines.cancellation.CancellationException\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nclass DeveloperProfileViewModel(\n    private val username: String,\n    private val repository: DeveloperProfileRepository,\n    private val favouritesRepository: FavouritesRepository,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n\n    private val _state = MutableStateFlow(DeveloperProfileState(username = username))\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    loadDeveloperData()\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = DeveloperProfileState(username = username),\n            )\n\n    private fun loadDeveloperData() {\n        viewModelScope.launch {\n            try {\n                _state.update {\n                    it.copy(\n                        isLoading = true,\n                        isLoadingRepos = false,\n                        errorMessage = null,\n                    )\n                }\n\n                val profileResult = repository.getDeveloperProfile(username)\n                profileResult\n                    .onSuccess { profile ->\n                        _state.update {\n                            it.copy(\n                                profile = profile,\n                                isLoading = false,\n                                isLoadingRepos = true,\n                            )\n                        }\n                    }.onFailure { error ->\n                        _state.update {\n                            it.copy(\n                                isLoading = false,\n                                errorMessage =\n                                    error.message\n                                        ?: getString(Res.string.failed_to_load_profile),\n                            )\n                        }\n                        return@launch\n                    }\n\n                val reposResult = repository.getDeveloperRepositories(username)\n\n                reposResult\n                    .onSuccess { repos ->\n                        _state.update {\n                            it.copy(\n                                repositories = repos.toImmutableList(),\n                                isLoading = false,\n                                isLoadingRepos = false,\n                            )\n                        }\n                        applyFiltersAndSort()\n                    }.onFailure { error ->\n                        _state.update {\n                            it.copy(\n                                isLoading = false,\n                                isLoadingRepos = false,\n                                errorMessage =\n                                    error.message\n                                        ?: getString(Res.string.failed_to_load_repositories),\n                            )\n                        }\n                    }\n            } catch (_: RateLimitException) {\n                _state.update {\n                    it.copy(isLoading = false, isLoadingRepos = false)\n                }\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                _state.update {\n                    it.copy(\n                        isLoading = false,\n                        isLoadingRepos = false,\n                        errorMessage = e.message,\n                    )\n                }\n            }\n        }\n    }\n\n    private fun applyFiltersAndSort() {\n        viewModelScope.launch(Dispatchers.Default) {\n            val currentState = _state.value\n            var filtered = currentState.repositories\n\n            if (currentState.searchQuery.isNotBlank()) {\n                val query = currentState.searchQuery.lowercase()\n                filtered =\n                    filtered\n                        .filter { repo ->\n                            repo.name.lowercase().contains(query) ||\n                                repo.description?.lowercase()?.contains(query) == true\n                        }.toImmutableList()\n            }\n\n            filtered =\n                when (currentState.currentFilter) {\n                    RepoFilterType.WITH_RELEASES -> {\n                        filtered\n                            .filter { it.hasInstallableAssets }\n                            .toImmutableList()\n                    }\n\n                    RepoFilterType.INSTALLED -> {\n                        filtered.filter { it.isInstalled }.toImmutableList()\n                    }\n\n                    RepoFilterType.FAVORITES -> {\n                        filtered.filter { it.isFavorite }.toImmutableList()\n                    }\n                }\n\n            filtered =\n                when (currentState.currentSort) {\n                    RepoSortType.UPDATED -> {\n                        filtered\n                            .sortedByDescending { it.updatedAt }\n                            .toImmutableList()\n                    }\n\n                    RepoSortType.STARS -> {\n                        filtered\n                            .sortedByDescending { it.stargazersCount }\n                            .toImmutableList()\n                    }\n\n                    RepoSortType.NAME -> {\n                        filtered.sortedBy { it.name.lowercase() }.toImmutableList()\n                    }\n                }\n\n            _state.update { it.copy(filteredRepositories = filtered) }\n        }\n    }\n\n    fun onAction(action: DeveloperProfileAction) {\n        when (action) {\n            DeveloperProfileAction.OnNavigateBackClick,\n            is DeveloperProfileAction.OnRepositoryClick,\n            is DeveloperProfileAction.OnOpenLink,\n            -> {\n            }\n\n            is DeveloperProfileAction.OnFilterChange -> {\n                _state.update { it.copy(currentFilter = action.filter) }\n                applyFiltersAndSort()\n            }\n\n            is DeveloperProfileAction.OnSortChange -> {\n                _state.update { it.copy(currentSort = action.sort) }\n                applyFiltersAndSort()\n            }\n\n            is DeveloperProfileAction.OnSearchQueryChange -> {\n                _state.update { it.copy(searchQuery = action.query) }\n                applyFiltersAndSort()\n            }\n\n            is DeveloperProfileAction.OnToggleFavorite -> {\n                viewModelScope.launch {\n                    val repo = action.repository\n\n                    val favoriteRepo =\n                        FavoriteRepo(\n                            repoId = repo.id,\n                            repoName = repo.name,\n                            repoOwner = repo.fullName.split(\"/\")[0],\n                            repoOwnerAvatarUrl = _state.value.profile?.avatarUrl ?: \"\",\n                            repoDescription = repo.description,\n                            primaryLanguage = repo.language,\n                            repoUrl = repo.htmlUrl,\n                            latestVersion = repo.latestVersion,\n                            latestReleaseUrl = null,\n                            addedAt = Clock.System.now().toEpochMilliseconds(),\n                            lastSyncedAt = Clock.System.now().toEpochMilliseconds(),\n                        )\n\n                    favouritesRepository.toggleFavorite(favoriteRepo)\n\n                    _state.update { state ->\n                        val updatedRepos =\n                            state.repositories\n                                .map {\n                                    if (it.id == repo.id) {\n                                        it.copy(isFavorite = !it.isFavorite)\n                                    } else {\n                                        it\n                                    }\n                                }.toImmutableList()\n\n                        state.copy(repositories = updatedRepos)\n                    }\n                    applyFiltersAndSort()\n                }\n            }\n\n            DeveloperProfileAction.OnDismissError -> {\n                _state.update { it.copy(errorMessage = null) }\n            }\n\n            DeveloperProfileAction.OnRetry -> {\n                loadDeveloperData()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage zed.rainxch.devprofile.presentation.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.CallSplit\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material.icons.outlined.FavoriteBorder\nimport androidx.compose.material.icons.outlined.Warning\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledIconToggleButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialShapes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SuggestionChip\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.toShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.formatCount\nimport zed.rainxch.devprofile.domain.model.DeveloperRepository\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun DeveloperRepoItem(\n    repository: DeveloperRepository,\n    onItemClick: () -> Unit,\n    onToggleFavorite: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    ExpressiveCard(\n        onClick = onItemClick,\n        modifier = modifier.fillMaxWidth(),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = repository.name,\n                        maxLines = 1,\n                        style = MaterialTheme.typography.titleLarge,\n                        overflow = TextOverflow.Ellipsis,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n\n                    Text(\n                        text =\n                            stringResource(\n                                resource = Res.string.updated_on_date,\n                                formatRelativeDate(repository.updatedAt),\n                            ).replaceFirstChar { it.uppercase() },\n                        style = MaterialTheme.typography.titleMedium,\n                        overflow = TextOverflow.Ellipsis,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        maxLines = 1,\n                    )\n                }\n\n                Spacer(modifier = Modifier.width(8.dp))\n\n                FilledIconToggleButton(\n                    checked = repository.isFavorite,\n                    onCheckedChange = { onToggleFavorite() },\n                    modifier = Modifier.size(40.dp),\n                    shape = MaterialShapes.Cookie6Sided.toShape(),\n                ) {\n                    Icon(\n                        imageVector =\n                            if (repository.isFavorite) {\n                                Icons.Filled.Favorite\n                            } else {\n                                Icons.Outlined.FavoriteBorder\n                            },\n                        contentDescription =\n                            if (repository.isFavorite) {\n                                stringResource(Res.string.remove_from_favourites)\n                            } else {\n                                stringResource(Res.string.add_to_favourites)\n                            },\n                        modifier = Modifier.size(20.dp),\n                    )\n                }\n            }\n\n            repository.description?.let { description ->\n                Spacer(modifier = Modifier.height(8.dp))\n\n                Text(\n                    text = description,\n                    maxLines = 2,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n\n            Spacer(modifier = Modifier.height(12.dp))\n\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .horizontalScroll(rememberScrollState()),\n                horizontalArrangement = Arrangement.spacedBy(16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                RepoStat(\n                    icon = Icons.Default.Star,\n                    value = formatCount(repository.stargazersCount),\n                    contentDescription = $$\"$${repository.stargazersCount} $${stringResource(Res.string.stars)}\",\n                )\n\n                RepoStat(\n                    icon = Icons.AutoMirrored.Filled.CallSplit,\n                    value = formatCount(repository.forksCount),\n                    contentDescription = \"${repository.forksCount} ${stringResource(Res.string.forks)}\",\n                )\n\n                if (repository.openIssuesCount > 0) {\n                    RepoStat(\n                        icon = Icons.Outlined.Warning,\n                        value = formatCount(repository.openIssuesCount),\n                        contentDescription = \"${repository.openIssuesCount} ${stringResource(Res.string.issues)}\",\n                    )\n                }\n\n                repository.language?.let { language ->\n                    SuggestionChip(\n                        onClick = {},\n                        label = {\n                            Text(\n                                text = language,\n                                style = MaterialTheme.typography.labelSmall,\n                            )\n                        },\n                        modifier = Modifier.height(32.dp),\n                    )\n                }\n            }\n\n            val repoBadges =\n                buildList {\n                    if (repository.hasInstallableAssets) {\n                        add(\n                            RepoBadge(\n                                text =\n                                    repository.latestVersion\n                                        ?: stringResource(Res.string.has_release),\n                                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                                contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                            ),\n                        )\n                    }\n                    if (repository.isInstalled) {\n                        add(\n                            RepoBadge(\n                                text = stringResource(Res.string.installed),\n                                containerColor = MaterialTheme.colorScheme.tertiaryContainer,\n                                contentColor = MaterialTheme.colorScheme.onTertiaryContainer,\n                            ),\n                        )\n                    }\n                }\n\n            if (repoBadges.isNotEmpty()) {\n                Spacer(modifier = Modifier.height(12.dp))\n\n                FlowRow(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    repoBadges.forEach { badge ->\n                        Badge(\n                            containerColor = badge.containerColor,\n                        ) {\n                            Text(\n                                text = badge.text,\n                                style = MaterialTheme.typography.labelSmall,\n                                color = badge.contentColor,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RepoStat(\n    icon: ImageVector,\n    value: String,\n    contentDescription: String,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.spacedBy(4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = contentDescription,\n            modifier = Modifier.size(16.dp),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n\n        Text(\n            text = value,\n            maxLines = 1,\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n    }\n}\n\nprivate data class RepoBadge(\n    val text: String,\n    val containerColor: Color,\n    val contentColor: Color,\n)\n\n@Composable\nprivate fun formatRelativeDate(dateString: String): String {\n    val instant =\n        try {\n            Instant.parse(dateString)\n        } catch (_: IllegalArgumentException) {\n            return dateString\n        }\n    val now = Clock.System.now()\n    val duration = now - instant\n\n    return when {\n        duration.inWholeDays > 365 -> {\n            stringResource(\n                Res.string.time_years_ago,\n                (duration.inWholeDays / 365).toInt(),\n            )\n        }\n\n        duration.inWholeDays > 30 -> {\n            stringResource(\n                Res.string.time_months_ago,\n                (duration.inWholeDays / 30).toInt(),\n            )\n        }\n\n        duration.inWholeDays > 0 -> {\n            stringResource(\n                Res.string.time_days_ago,\n                duration.inWholeDays.toInt(),\n            )\n        }\n\n        duration.inWholeHours > 0 -> {\n            stringResource(\n                Res.string.time_hours_ago,\n                duration.inWholeHours.toInt(),\n            )\n        }\n\n        duration.inWholeMinutes > 0 -> {\n            stringResource(\n                Res.string.time_minutes_ago,\n                duration.inWholeMinutes.toInt(),\n            )\n        }\n\n        else -> {\n            stringResource(Res.string.just_now)\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewDeveloperRepoItem() {\n    GithubStoreTheme {\n        DeveloperRepoItem(\n            repository =\n                DeveloperRepository(\n                    id = 1,\n                    name = \"awesome-kotlin-app\",\n                    fullName = \"developer/awesome-kotlin-app\",\n                    description = \"An amazing Kotlin Multiplatform application that demonstrates modern Android development\",\n                    htmlUrl = \"\",\n                    stargazersCount = 2340,\n                    forksCount = 456,\n                    openIssuesCount = 23,\n                    language = \"Kotlin\",\n                    hasReleases = true,\n                    hasInstallableAssets = true,\n                    isInstalled = true,\n                    isFavorite = false,\n                    latestVersion = \"v1.5.2\",\n                    updatedAt = Clock.System.now().toString(),\n                    pushedAt = null,\n                ),\n            onItemClick = {},\n            onToggleFavorite = {},\n        )\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt",
    "content": "package zed.rainxch.devprofile.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Sort\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledIconButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialShapes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.SecondaryScrollableTabRow\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.toShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.devprofile.domain.model.RepoFilterType\nimport zed.rainxch.devprofile.domain.model.RepoSortType\nimport zed.rainxch.devprofile.presentation.DeveloperProfileAction\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun FilterSortControls(\n    currentFilter: RepoFilterType,\n    currentSort: RepoSortType,\n    searchQuery: String,\n    repoCount: Int,\n    totalCount: Int,\n    onAction: (DeveloperProfileAction) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        verticalArrangement = Arrangement.spacedBy(12.dp),\n    ) {\n        OutlinedTextField(\n            value = searchQuery,\n            onValueChange = { query ->\n                onAction(DeveloperProfileAction.OnSearchQueryChange(query))\n            },\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = {\n                Text(\n                    text = stringResource(Res.string.search_repositories),\n                    maxLines = 1,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            },\n            leadingIcon = {\n                Icon(\n                    imageVector = Icons.Default.Search,\n                    contentDescription = stringResource(Res.string.search_repositories),\n                    modifier = Modifier.size(20.dp),\n                )\n            },\n            trailingIcon = {\n                if (searchQuery.isNotBlank()) {\n                    IconButton(\n                        onClick = { onAction(DeveloperProfileAction.OnSearchQueryChange(\"\")) },\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Close,\n                            contentDescription = stringResource(Res.string.clear_search),\n                            modifier = Modifier.size(20.dp),\n                        )\n                    }\n                }\n            },\n            singleLine = true,\n            shape = RoundedCornerShape(12.dp),\n        )\n\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            SecondaryScrollableTabRow(\n                selectedTabIndex = currentFilter.ordinal,\n                modifier = Modifier.weight(1f),\n                edgePadding = 0.dp,\n                divider = {},\n            ) {\n                RepoFilterType.entries.forEach { filter ->\n                    FilterChipTab(\n                        selected = currentFilter == filter,\n                        onClick = { onAction(DeveloperProfileAction.OnFilterChange(filter)) },\n                        label = filter.displayName(),\n                    )\n                }\n            }\n\n            SortMenu(\n                currentSort = currentSort,\n                onSortChange = { sort ->\n                    onAction(DeveloperProfileAction.OnSortChange(sort))\n                },\n            )\n        }\n\n        Text(\n            text =\n                if (repoCount == totalCount) {\n                    \"$repoCount ${\n                        stringResource(\n                            if (repoCount == 1) {\n                                Res.string.repository_singular\n                            } else {\n                                Res.string.repositories\n                            },\n                        )\n                    }\"\n                } else {\n                    stringResource(\n                        resource = Res.string.showing_x_of_y_repositories,\n                        repoCount,\n                        totalCount,\n                    )\n                },\n            maxLines = 1,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n    }\n}\n\n@Composable\nprivate fun FilterChipTab(\n    selected: Boolean,\n    onClick: () -> Unit,\n    label: String,\n) {\n    Tab(\n        selected = selected,\n        onClick = onClick,\n        modifier = Modifier.height(40.dp),\n    ) {\n        Text(\n            text = label,\n            style = MaterialTheme.typography.labelMedium,\n            color =\n                if (selected) {\n                    MaterialTheme.colorScheme.primary\n                } else {\n                    MaterialTheme.colorScheme.onSurfaceVariant\n                },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun SortMenu(\n    currentSort: RepoSortType,\n    onSortChange: (RepoSortType) -> Unit,\n) {\n    var expanded by remember { mutableStateOf(false) }\n\n    Box {\n        FilledIconButton(\n            onClick = { expanded = true },\n            modifier = Modifier.size(40.dp),\n            shape = MaterialShapes.Cookie9Sided.toShape(),\n        ) {\n            Icon(\n                imageVector = Icons.AutoMirrored.Filled.Sort,\n                contentDescription = stringResource(Res.string.sort),\n                modifier = Modifier.size(20.dp),\n            )\n        }\n\n        DropdownMenu(\n            expanded = expanded,\n            onDismissRequest = { expanded = false },\n            shape = RoundedCornerShape(32.dp),\n        ) {\n            RepoSortType.entries.forEach { sort ->\n                DropdownMenuItem(\n                    text = {\n                        Row(\n                            modifier = Modifier.padding(4.dp),\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            if (currentSort == sort) {\n                                Icon(\n                                    imageVector = Icons.Default.Check,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                    tint = MaterialTheme.colorScheme.primary,\n                                )\n                            } else {\n                                Spacer(modifier = Modifier.size(18.dp))\n                            }\n\n                            Text(\n                                text = sort.displayName(),\n                                maxLines = 1,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color =\n                                    if (currentSort == sort) {\n                                        MaterialTheme.colorScheme.primary\n                                    } else {\n                                        MaterialTheme.colorScheme.onSurface\n                                    },\n                            )\n                        }\n                    },\n                    onClick = {\n                        onSortChange(sort)\n                        expanded = false\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RepoFilterType.displayName(): String =\n    when (this) {\n        RepoFilterType.WITH_RELEASES -> stringResource(Res.string.filter_with_releases)\n        RepoFilterType.INSTALLED -> stringResource(Res.string.filter_installed)\n        RepoFilterType.FAVORITES -> stringResource(Res.string.filter_favorites)\n    }\n\n@Composable\nprivate fun RepoSortType.displayName(): String =\n    when (this) {\n        RepoSortType.UPDATED -> stringResource(Res.string.sort_recently_updated)\n        RepoSortType.STARS -> stringResource(Res.string.sort_most_stars)\n        RepoSortType.NAME -> stringResource(Res.string.sort_name)\n    }\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt",
    "content": "package zed.rainxch.devprofile.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Business\nimport androidx.compose.material.icons.filled.Link\nimport androidx.compose.material.icons.filled.LocationOn\nimport androidx.compose.material.icons.filled.Tag\nimport androidx.compose.material3.AssistChip\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.ImageOptions\nimport com.skydoves.landscapist.coil3.CoilImage\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.devprofile.domain.model.DeveloperProfile\nimport zed.rainxch.devprofile.presentation.DeveloperProfileAction\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ProfileInfoCard(\n    profile: DeveloperProfile,\n    onAction: (DeveloperProfileAction) -> Unit,\n) {\n    ExpressiveCard {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.Top,\n            ) {\n                CoilImage(\n                    imageModel = { profile.avatarUrl },\n                    modifier =\n                        Modifier\n                            .size(80.dp)\n                            .clip(CircleShape),\n                    imageOptions =\n                        ImageOptions(\n                            contentScale = ContentScale.Crop,\n                        ),\n                )\n\n                Spacer(modifier = Modifier.width(16.dp))\n\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = profile.name ?: profile.login,\n                        maxLines = 2,\n                        style = MaterialTheme.typography.titleLarge,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n\n                    Spacer(Modifier.height(4.dp))\n\n                    Text(\n                        text = \"@${profile.login}\",\n                        maxLines = 1,\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n\n                    profile.location?.let { location ->\n                        Spacer(Modifier.height(8.dp))\n\n                        InfoChip(\n                            icon = Icons.Default.LocationOn,\n                            text = location,\n                        )\n                    }\n\n                    profile.bio?.let { bio ->\n                        Spacer(modifier = Modifier.height(8.dp))\n\n                        Text(\n                            text = bio,\n                            maxLines = 4,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n\n            Spacer(modifier = Modifier.height(12.dp))\n\n            FlowRow(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n                itemVerticalAlignment = Alignment.CenterVertically,\n            ) {\n                profile.company?.let { company ->\n                    InfoChip(\n                        icon = Icons.Default.Business,\n                        text = company,\n                    )\n                }\n\n                profile.blog?.takeIf { it.isNotBlank() }?.let { blog ->\n                    val displayUrl = blog.removePrefix(\"https://\").removePrefix(\"http://\")\n                    AssistChip(\n                        onClick = {\n                            val url = if (!blog.startsWith(\"http\")) \"https://$blog\" else blog\n                            onAction(DeveloperProfileAction.OnOpenLink(url))\n                        },\n                        label = {\n                            Text(\n                                text = displayUrl,\n                                style = MaterialTheme.typography.labelMedium,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        },\n                        leadingIcon = {\n                            Icon(\n                                imageVector = Icons.Default.Link,\n                                contentDescription = null,\n                                modifier = Modifier.size(16.dp),\n                            )\n                        },\n                    )\n                }\n\n                profile.twitterUsername?.let { twitter ->\n                    AssistChip(\n                        onClick = {\n                            onAction(DeveloperProfileAction.OnOpenLink(\"https://twitter.com/$twitter\"))\n                        },\n                        label = {\n                            Text(\n                                text = \"@$twitter\",\n                                style = MaterialTheme.typography.labelMedium,\n                            )\n                        },\n                        leadingIcon = {\n                            Icon(\n                                imageVector = Icons.Default.Tag,\n                                contentDescription = null,\n                                modifier = Modifier.size(16.dp),\n                            )\n                        },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InfoChip(\n    icon: ImageVector,\n    text: String,\n) {\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n            modifier = Modifier.size(16.dp),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n\n        Text(\n            text = text,\n            maxLines = 1,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n    }\n}\n"
  },
  {
    "path": "feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/StatsRow.kt",
    "content": "package zed.rainxch.devprofile.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.core.presentation.utils.formatCount\nimport zed.rainxch.devprofile.domain.model.DeveloperProfile\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun StatsRow(profile: DeveloperProfile) {\n    Row(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        StatCard(\n            label = stringResource(Res.string.repositories),\n            value = profile.publicRepos.toString(),\n            modifier = Modifier.weight(1f),\n        )\n\n        StatCard(\n            label = stringResource(Res.string.followers),\n            value = formatCount(profile.followers),\n            modifier = Modifier.weight(1f),\n        )\n\n        StatCard(\n            label = stringResource(Res.string.following),\n            value = formatCount(profile.following),\n            modifier = Modifier.weight(1f),\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun StatCard(\n    label: String,\n    value: String,\n    modifier: Modifier = Modifier,\n) {\n    ExpressiveCard(modifier = modifier) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Text(\n                text = value,\n                maxLines = 1,\n                style = MaterialTheme.typography.titleLarge,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n\n            Text(\n                text = label,\n                maxLines = 1,\n                style = MaterialTheme.typography.bodyMedium,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/favourites/CLAUDE.md",
    "content": "# CLAUDE.md - Favourites Feature\n\n## Purpose\n\nDisplays the user's locally saved favorite repositories. This is a **presentation-only** feature with no domain or data layer -- it uses `FavouritesRepository` from `core/domain` directly.\n\n## Module Structure\n\n```\nfeature/favourites/\n└── presentation/\n    ├── FavouritesViewModel.kt         # Observes favourites, handles remove\n    ├── FavouritesState.kt             # favourites list, loading\n    ├── FavouritesAction.kt            # RemoveFavourite, click actions\n    ├── FavouritesRoot.kt              # Main composable (list of favourites)\n    ├── model/FavouriteRepository.kt   # UI model for display\n    ├── mappers/FavouriteRepositoryMapper.kt  # Domain → UI model mapper\n    └── components/FavouriteRepositoryItem.kt  # Individual favourite card\n```\n\n## Key Dependencies\n\n- `FavouritesRepository` (from `core/domain`) - CRUD operations for favourites\n- Favourites are stored locally in Room database (`FavoriteRepoDao` in `core/data`)\n\n## Navigation\n\nRoute: `GithubStoreGraph.FavouritesScreen` (data object, no params)\n\n## Implementation Notes\n\n- No network calls -- all data is local (Room database)\n- Uses a presentation-layer `FavouriteRepository` UI model mapped from the domain `FavoriteRepo`\n- Adding to favourites happens in other features (home, details, search); this feature only displays and removes\n- The Koin module for this feature is registered in `composeApp/.../app/di/ViewModelsModule.kt` since there's no `data/di/` layer\n"
  },
  {
    "path": "feature/favourites/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/favourites/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.feature.favourites.domain)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/favourites/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/favourites/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/favourites/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/favourites/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/favourites/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/favourites/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.favourites.domain)\n\n                implementation(libs.bundles.landscapist)\n                implementation(libs.kotlinx.collections.immutable)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/favourites/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesAction.kt",
    "content": "package zed.rainxch.favourites.presentation\n\nimport zed.rainxch.favourites.presentation.model.FavouriteRepository\n\nsealed interface FavouritesAction {\n    data object OnNavigateBackClick : FavouritesAction\n\n    data class OnToggleFavorite(\n        val favouriteRepository: FavouriteRepository,\n    ) : FavouritesAction\n\n    data class OnRepositoryClick(\n        val favouriteRepository: FavouriteRepository,\n    ) : FavouritesAction\n\n    data class OnDeveloperProfileClick(\n        val username: String,\n    ) : FavouritesAction\n}\n"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt",
    "content": "package zed.rainxch.favourites.presentation\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid\nimport androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells\nimport androidx.compose.foundation.lazy.staggeredgrid.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.favourites.presentation.components.FavouriteRepositoryItem\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@Composable\nfun FavouritesRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToDetails: (repoId: Long) -> Unit,\n    onNavigateToDeveloperProfile: (username: String) -> Unit,\n    viewModel: FavouritesViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n\n    FavouritesScreen(\n        state = state,\n        onAction = { action ->\n            when (action) {\n                FavouritesAction.OnNavigateBackClick -> {\n                    onNavigateBack()\n                }\n\n                is FavouritesAction.OnRepositoryClick -> {\n                    onNavigateToDetails(action.favouriteRepository.repoId)\n                }\n\n                is FavouritesAction.OnDeveloperProfileClick -> {\n                    onNavigateToDeveloperProfile(action.username)\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun FavouritesScreen(\n    state: FavouritesState,\n    onAction: (FavouritesAction) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            FavouritesTopbar(onAction)\n        },\n        containerColor = MaterialTheme.colorScheme.background,\n    ) { innerPadding ->\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n        ) {\n            LazyVerticalStaggeredGrid(\n                columns =\n                    StaggeredGridCells.Adaptive(\n                        350.dp,\n                    ),\n                verticalItemSpacing = 12.dp,\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n                contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp),\n                modifier = Modifier.fillMaxSize(),\n            ) {\n                items(\n                    items = state.favouriteRepositories,\n                    key = { it.repoId },\n                ) { repo ->\n                    FavouriteRepositoryItem(\n                        favouriteRepository = repo,\n                        onToggleFavouriteClick = {\n                            onAction(FavouritesAction.OnToggleFavorite(repo))\n                        },\n                        onItemClick = {\n                            onAction(FavouritesAction.OnRepositoryClick(repo))\n                        },\n                        onDevProfileClick = {\n                            onAction(FavouritesAction.OnDeveloperProfileClick(repo.repoOwner))\n                        },\n                        modifier = Modifier.animateItem(),\n                    )\n                }\n            }\n\n            if (state.isLoading) {\n                CircularWavyProgressIndicator(\n                    modifier = Modifier.align(Alignment.Center),\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun FavouritesTopbar(onAction: (FavouritesAction) -> Unit) {\n    TopAppBar(\n        title = {\n            Text(\n                text = stringResource(Res.string.favourites),\n                style = MaterialTheme.typography.titleMediumEmphasized,\n                fontWeight = FontWeight.SemiBold,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n        },\n        navigationIcon = {\n            IconButton(\n                shapes = IconButtonDefaults.shapes(),\n                onClick = {\n                    onAction(FavouritesAction.OnNavigateBackClick)\n                },\n            ) {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                    contentDescription = stringResource(Res.string.navigate_back),\n                    modifier = Modifier.size(24.dp),\n                )\n            }\n        },\n    )\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        FavouritesScreen(\n            state = FavouritesState(),\n            onAction = {},\n        )\n    }\n}\n"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesState.kt",
    "content": "package zed.rainxch.favourites.presentation\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport zed.rainxch.favourites.presentation.model.FavouriteRepository\n\ndata class FavouritesState(\n    val favouriteRepositories: ImmutableList<FavouriteRepository> = persistentListOf(),\n    val isLoading: Boolean = false,\n)\n"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt",
    "content": "package zed.rainxch.favourites.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.collections.immutable.toImmutableList\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport zed.rainxch.core.domain.model.FavoriteRepo\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.favourites.presentation.mappers.toFavouriteRepositoryUi\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nclass FavouritesViewModel(\n    private val favouritesRepository: FavouritesRepository,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n\n    private val _state = MutableStateFlow(FavouritesState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    loadFavouriteRepos()\n\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = FavouritesState(),\n            )\n\n    private fun loadFavouriteRepos() {\n        viewModelScope.launch {\n            favouritesRepository\n                .getAllFavorites()\n                .map { it.map { it.toFavouriteRepositoryUi() } }\n                .flowOn(Dispatchers.Default)\n                .collect { favoriteRepos ->\n                    _state.update {\n                        it.copy(\n                            favouriteRepositories = favoriteRepos.toImmutableList(),\n                        )\n                    }\n                }\n        }\n    }\n\n    @OptIn(ExperimentalTime::class)\n    fun onAction(action: FavouritesAction) {\n        when (action) {\n            FavouritesAction.OnNavigateBackClick -> {\n                // Handled in composable\n            }\n\n            is FavouritesAction.OnRepositoryClick -> {\n                // Handled in composable\n            }\n\n            is FavouritesAction.OnDeveloperProfileClick -> {\n                // Handled in composable\n            }\n\n            is FavouritesAction.OnToggleFavorite -> {\n                viewModelScope.launch {\n                    val repo = action.favouriteRepository\n\n                    val favoriteRepo =\n                        FavoriteRepo(\n                            repoId = repo.repoId,\n                            repoName = repo.repoName,\n                            repoOwner = repo.repoOwner,\n                            repoOwnerAvatarUrl = repo.repoOwnerAvatarUrl,\n                            repoDescription = repo.repoDescription,\n                            primaryLanguage = repo.primaryLanguage,\n                            repoUrl = repo.repoUrl,\n                            latestVersion = repo.latestRelease,\n                            latestReleaseUrl = repo.latestReleaseUrl,\n                            addedAt = Clock.System.now().toEpochMilliseconds(),\n                            lastSyncedAt = Clock.System.now().toEpochMilliseconds(),\n                        )\n\n                    favouritesRepository.toggleFavorite(favoriteRepo)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt",
    "content": "package zed.rainxch.favourites.presentation.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.CalendarToday\nimport androidx.compose.material.icons.filled.Code\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.NewReleases\nimport androidx.compose.material3.AssistChip\nimport androidx.compose.material3.AssistChipDefaults\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialShapes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.toShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.coil3.CoilImage\nimport com.skydoves.landscapist.components.rememberImageComponent\nimport com.skydoves.landscapist.crossfade.CrossfadePlugin\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.favourites.presentation.model.FavouriteRepository\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.remove_from_favourites\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun FavouriteRepositoryItem(\n    favouriteRepository: FavouriteRepository,\n    onToggleFavouriteClick: () -> Unit,\n    onItemClick: () -> Unit,\n    onDevProfileClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    ExpressiveCard(\n        modifier = modifier,\n        onClick = onItemClick,\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n        ) {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clip(RoundedCornerShape(24.dp))\n                        .clickable(onClick = onDevProfileClick),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                CoilImage(\n                    imageModel = { favouriteRepository.repoOwnerAvatarUrl },\n                    modifier =\n                        Modifier\n                            .size(32.dp)\n                            .clip(CircleShape),\n                    loading = {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            CircularWavyProgressIndicator()\n                        }\n                    },\n                    component =\n                        rememberImageComponent {\n                            CrossfadePlugin()\n                        },\n                )\n\n                Text(\n                    text = favouriteRepository.repoOwner,\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.outline,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    modifier = Modifier.weight(1f),\n                )\n            }\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.Top,\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                Column(\n                    modifier = Modifier.weight(1f),\n                ) {\n                    Text(\n                        text = favouriteRepository.repoName,\n                        fontWeight = FontWeight.Bold,\n                        style = MaterialTheme.typography.titleLarge,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n\n                    favouriteRepository.repoDescription?.let {\n                        Spacer(modifier = Modifier.height(4.dp))\n\n                        Text(\n                            text = it,\n                            fontWeight = FontWeight.Medium,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            maxLines = 2,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n                }\n\n                IconButton(\n                    onClick = onToggleFavouriteClick,\n                    colors = IconButtonDefaults.filledTonalIconButtonColors(),\n                    modifier = Modifier.align(Alignment.CenterVertically),\n                    shape = MaterialShapes.Cookie6Sided.toShape(),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Favorite,\n                        contentDescription = stringResource(Res.string.remove_from_favourites),\n                    )\n                }\n            }\n\n            Spacer(modifier = Modifier.height(12.dp))\n\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .horizontalScroll(rememberScrollState()),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                favouriteRepository.primaryLanguage?.let { language ->\n                    AssistChip(\n                        onClick = { /* No action */ },\n                        label = {\n                            Text(\n                                text = language,\n                                style = MaterialTheme.typography.titleSmall,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        },\n                        leadingIcon = {\n                            Icon(\n                                imageVector = Icons.Default.Code,\n                                contentDescription = null,\n                                modifier = Modifier.size(AssistChipDefaults.IconSize),\n                            )\n                        },\n                        colors =\n                            AssistChipDefaults.assistChipColors(\n                                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                                labelColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                                leadingIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                            ),\n                    )\n                }\n\n                favouriteRepository.latestRelease?.let { release ->\n                    AssistChip(\n                        onClick = { /* No action */ },\n                        label = {\n                            Text(\n                                text = release,\n                                style = MaterialTheme.typography.titleSmall,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        },\n                        leadingIcon = {\n                            Icon(\n                                imageVector = Icons.Default.NewReleases,\n                                contentDescription = null,\n                                modifier = Modifier.size(AssistChipDefaults.IconSize),\n                            )\n                        },\n                    )\n                }\n\n                AssistChip(\n                    onClick = { /* No action */ },\n                    label = {\n                        Text(\n                            text = favouriteRepository.addedAtFormatter,\n                            style = MaterialTheme.typography.titleSmall,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    },\n                    leadingIcon = {\n                        Icon(\n                            imageVector = Icons.Default.CalendarToday,\n                            contentDescription = null,\n                            modifier = Modifier.size(AssistChipDefaults.IconSize),\n                        )\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/mappers/FavouriteRepositoryMapper.kt",
    "content": "package zed.rainxch.favourites.presentation.mappers\n\nimport zed.rainxch.core.domain.model.FavoriteRepo\nimport zed.rainxch.core.presentation.utils.formatAddedAt\nimport zed.rainxch.favourites.presentation.model.FavouriteRepository\n\nsuspend fun FavoriteRepo.toFavouriteRepositoryUi(): FavouriteRepository =\n    FavouriteRepository(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        latestRelease = latestVersion,\n        latestReleaseUrl = latestReleaseUrl,\n        addedAtFormatter = formatAddedAt(addedAt),\n    )\n"
  },
  {
    "path": "feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/model/FavouriteRepository.kt",
    "content": "package zed.rainxch.favourites.presentation.model\n\ndata class FavouriteRepository(\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val addedAtFormatter: String,\n    val latestRelease: String?,\n    val latestReleaseUrl: String?,\n)\n"
  },
  {
    "path": "feature/home/CLAUDE.md",
    "content": "# CLAUDE.md - Home Feature\n\n## Purpose\n\nMain discovery screen of the app. Displays repositories in three categories: **Trending**, **Hot Releases**, and **Most Popular**. Supports infinite-scroll pagination and integrates with installed apps, favourites, and starred status.\n\n## Module Structure\n\n```\nfeature/home/\n├── domain/\n│   ├── model/HomeCategory.kt          # Enum: TRENDING, HOT_RELEASE, MOST_POPULAR\n│   └── repository/HomeRepository.kt   # Paginated flows per category\n├── data/\n│   ├── di/SharedModule.kt             # Koin: homeModule\n│   ├── repository/HomeRepositoryImpl.kt  # GitHub API calls with caching & pagination\n│   ├── data_source/CachedRepositoriesDataSource.kt  # Per-category cache (7-day expiry)\n│   ├── dto/                           # Network DTOs\n│   └── mappers/                       # DTO → domain model mappers\n└── presentation/\n    ├── HomeViewModel.kt               # State management, pagination logic\n    ├── HomeState.kt                   # repos, isLoading, category, hasMorePages, etc.\n    ├── HomeAction.kt                  # Refresh, Retry, LoadMore, SwitchCategory, clicks\n    ├── HomeEvent.kt                   # OnScrollToListTop\n    ├── HomeRoot.kt                    # Main composable (staggered grid + filter chips)\n    ├── components/HomeFilterChips.kt  # Category filter chip row\n    ├── locals/LocalHomeTopBarLiquid.kt\n    └── utils/HomeCategoryMapper.kt    # Map HomeCategory to display strings\n```\n\n## Key Interfaces\n\n```kotlin\ninterface HomeRepository {\n    fun getTrendingRepositories(page: Int): Flow<PaginatedDiscoveryRepositories>\n    fun getHotReleaseRepositories(page: Int): Flow<PaginatedDiscoveryRepositories>\n    fun getMostPopular(page: Int): Flow<PaginatedDiscoveryRepositories>\n}\n```\n\n## ViewModel Dependencies\n\n`HomeViewModel` depends on: `HomeRepository`, `InstalledAppsRepository`, `Platform`, `SyncInstalledAppsUseCase`, `FavouritesRepository`, `StarredRepository`, `GitHubStoreLogger`\n\n## Navigation\n\nRoute: `GithubStoreGraph.HomeScreen` (data object, no params)\n\n## Implementation Notes\n\n- Uses `Semaphore` in `HomeRepositoryImpl` for concurrent request control\n- Cache is per-category with 7-day TTL in `CachedRepositoriesDataSource`\n- Pagination uses `nextPageIndex` tracking; deduplicates by `fullName`\n- Apps section visibility is platform-dependent (`Platform.ANDROID` only)\n- Observes installed apps, favourites, and starred repos reactively to update status badges\n- State uses `onStart` + `stateIn(WhileSubscribed)` for lazy initialization\n"
  },
  {
    "path": "feature/home/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/home/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.home.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n                implementation(libs.kotlinx.datetime)\n\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/home/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/CachedRepositoriesDataSource.kt",
    "content": "package zed.rainxch.home.data.data_source\n\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.home.data.dto.CachedRepoResponse\n\ninterface CachedRepositoriesDataSource {\n    suspend fun getCachedTrendingRepos(platform: DiscoveryPlatform): CachedRepoResponse?\n\n    suspend fun getCachedHotReleaseRepos(platform: DiscoveryPlatform): CachedRepoResponse?\n\n    suspend fun getCachedMostPopularRepos(platform: DiscoveryPlatform): CachedRepoResponse?\n}\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt",
    "content": "package zed.rainxch.home.data.data_source.impl\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.HttpResponse\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.isSuccess\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.SerializationException\nimport kotlinx.serialization.json.Json\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.home.data.data_source.CachedRepositoriesDataSource\nimport zed.rainxch.home.data.dto.CachedGithubRepoSummary\nimport zed.rainxch.home.data.dto.CachedRepoResponse\nimport zed.rainxch.home.domain.model.HomeCategory\nimport kotlin.coroutines.cancellation.CancellationException\nimport kotlin.time.Clock\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\n@OptIn(ExperimentalTime::class)\nclass CachedRepositoriesDataSourceImpl(\n    private val logger: GitHubStoreLogger,\n) : CachedRepositoriesDataSource {\n    private val json =\n        Json {\n            ignoreUnknownKeys = true\n            isLenient = true\n        }\n\n    private val httpClient =\n        HttpClient {\n            install(HttpTimeout) {\n                requestTimeoutMillis = 10_000\n                connectTimeoutMillis = 5_000\n                socketTimeoutMillis = 10_000\n            }\n            expectSuccess = false\n        }\n\n    private val cacheMutex = Mutex()\n    private val memoryCache = mutableMapOf<CacheKey, CacheEntry>()\n\n    private data class CacheEntry(\n        val data: CachedRepoResponse,\n        val fetchedAt: Instant,\n    )\n\n    override suspend fun getCachedTrendingRepos(platform: DiscoveryPlatform): CachedRepoResponse? =\n        fetchCachedReposForCategory(platform, HomeCategory.TRENDING)\n\n    override suspend fun getCachedHotReleaseRepos(platform: DiscoveryPlatform): CachedRepoResponse? =\n        fetchCachedReposForCategory(platform, HomeCategory.HOT_RELEASE)\n\n    override suspend fun getCachedMostPopularRepos(platform: DiscoveryPlatform): CachedRepoResponse? =\n        fetchCachedReposForCategory(platform, HomeCategory.MOST_POPULAR)\n\n    private suspend fun fetchCachedReposForCategory(\n        platform: DiscoveryPlatform,\n        category: HomeCategory,\n    ): CachedRepoResponse? {\n        val cacheKey = CacheKey(platform, category)\n\n        val cached = cacheMutex.withLock { memoryCache[cacheKey] }\n        if (cached != null) {\n            val age = Clock.System.now() - cached.fetchedAt\n            if (age < CACHE_TTL) {\n                logger.debug(\"Memory cache hit for $cacheKey (age: ${age.inWholeSeconds}s)\")\n                return cached.data\n            } else {\n                logger.debug(\"Memory cache expired for $cacheKey (age: ${age.inWholeSeconds}s)\")\n            }\n        }\n\n        return withContext(Dispatchers.IO) {\n            val paths =\n                when (category) {\n                    HomeCategory.TRENDING -> {\n                        listOf(\n                            \"cached-data/trending/android.json\",\n                            \"cached-data/trending/windows.json\",\n                            \"cached-data/trending/macos.json\",\n                            \"cached-data/trending/linux.json\",\n                        )\n                    }\n\n                    HomeCategory.HOT_RELEASE -> {\n                        listOf(\n                            \"cached-data/new-releases/android.json\",\n                            \"cached-data/new-releases/windows.json\",\n                            \"cached-data/new-releases/macos.json\",\n                            \"cached-data/new-releases/linux.json\",\n                        )\n                    }\n\n                    HomeCategory.MOST_POPULAR -> {\n                        listOf(\n                            \"cached-data/most-popular/android.json\",\n                            \"cached-data/most-popular/windows.json\",\n                            \"cached-data/most-popular/macos.json\",\n                            \"cached-data/most-popular/linux.json\",\n                        )\n                    }\n                }\n\n            val responses =\n                coroutineScope {\n                    paths\n                        .map { path ->\n                            async {\n                                val url = \"https://raw.githubusercontent.com/OpenHub-Store/api/main/$path\"\n                                val filePlatform =\n                                    when {\n                                        path.contains(\"/android\") -> DiscoveryPlatform.Android\n                                        path.contains(\"/windows\") -> DiscoveryPlatform.Windows\n                                        path.contains(\"/macos\") -> DiscoveryPlatform.Macos\n                                        path.contains(\"/linux\") -> DiscoveryPlatform.Linux\n                                        else -> error(\"Unknown platform in path: $path\")\n                                    }\n                                try {\n                                    logger.debug(\"Fetching from: $url\")\n                                    val response: HttpResponse = httpClient.get(url)\n                                    if (response.status.isSuccess()) {\n                                        json\n                                            .decodeFromString<CachedRepoResponse>(response.bodyAsText())\n                                            .let { repoResponse ->\n                                                repoResponse.copy(\n                                                    repositories =\n                                                        repoResponse.repositories.map {\n                                                            it.copy(availablePlatforms = listOf(filePlatform))\n                                                        },\n                                                )\n                                            }\n                                    } else {\n                                        logger.error(\"HTTP ${response.status.value} from $url\")\n                                        null\n                                    }\n                                } catch (e: SerializationException) {\n                                    logger.error(\"Parse error from $url: ${e.message}\")\n                                    null\n                                } catch (e: CancellationException) {\n                                    throw e\n                                } catch (e: Exception) {\n                                    logger.error(\"Error with $url: ${e.message}\")\n                                    null\n                                }\n                            }\n                        }.awaitAll()\n                        .filterNotNull()\n                }\n\n            if (responses.isEmpty()) {\n                logger.error(\"All mirrors failed for $cacheKey\")\n                return@withContext null\n            }\n\n            val allMergedRepos =\n                responses\n                    .asSequence()\n                    .flatMap { it.repositories.asSequence() }\n                    .groupBy { it.id }\n                    .values\n                    .map { duplicates ->\n                        duplicates.reduce { acc, repo ->\n                            acc.copy(\n                                availablePlatforms = (acc.availablePlatforms + repo.availablePlatforms).distinct(),\n                                trendingScore =\n                                    listOfNotNull(\n                                        acc.trendingScore,\n                                        repo.trendingScore,\n                                    ).maxOrNull(),\n                                popularityScore =\n                                    listOfNotNull(\n                                        acc.popularityScore,\n                                        repo.popularityScore,\n                                    ).maxOrNull(),\n                                latestReleaseDate =\n                                    listOfNotNull(\n                                        acc.latestReleaseDate,\n                                        repo.latestReleaseDate,\n                                    ).maxOrNull(),\n                            )\n                        }\n                    }.sortedWith(\n                        compareByDescending<CachedGithubRepoSummary> { it.trendingScore }\n                            .thenByDescending { it.popularityScore }\n                            .thenByDescending { it.latestReleaseDate },\n                    )\n\n            val filteredRepos =\n                when (platform) {\n                    DiscoveryPlatform.All -> allMergedRepos\n                    else -> allMergedRepos.filter { platform in it.availablePlatforms }\n                }.toList()\n\n            val merged =\n                CachedRepoResponse(\n                    category = responses.first().category,\n                    platform = platform.name.lowercase(),\n                    lastUpdated = responses.maxOf { it.lastUpdated },\n                    totalCount = filteredRepos.size,\n                    repositories = filteredRepos,\n                )\n\n            if (responses.size == paths.size) {\n                cacheMutex.withLock {\n                    memoryCache[cacheKey] =\n                        CacheEntry(data = merged, fetchedAt = Clock.System.now())\n                }\n            }\n\n            merged\n        }\n    }\n\n    private companion object {\n        private val CACHE_TTL = 1.hours\n    }\n\n    private data class CacheKey(\n        val platform: DiscoveryPlatform,\n        val category: HomeCategory,\n    )\n}\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/di/SharedModule.kt",
    "content": "package zed.rainxch.home.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.home.data.data_source.CachedRepositoriesDataSource\nimport zed.rainxch.home.data.data_source.impl.CachedRepositoriesDataSourceImpl\nimport zed.rainxch.home.data.repository.HomeRepositoryImpl\nimport zed.rainxch.home.domain.repository.HomeRepository\n\nval homeModule =\n    module {\n        single<HomeRepository> {\n            HomeRepositoryImpl(\n                cachedDataSource = get(),\n                httpClient = get(),\n                devicePlatform = get(),\n                logger = get(),\n                cacheManager = get(),\n            )\n        }\n\n        single<CachedRepositoriesDataSource> {\n            CachedRepositoriesDataSourceImpl(\n                logger = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubOwner.kt",
    "content": "package zed.rainxch.home.data.dto\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class CachedGithubOwner(\n    val login: String,\n    val avatarUrl: String,\n)\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt",
    "content": "package zed.rainxch.home.data.dto\n\nimport kotlinx.serialization.Serializable\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\n\n@Serializable\ndata class CachedGithubRepoSummary(\n    val id: Long,\n    val name: String,\n    val fullName: String,\n    val owner: CachedGithubOwner,\n    val description: String?,\n    val defaultBranch: String,\n    val htmlUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val language: String?,\n    val topics: List<String>?,\n    val releasesUrl: String,\n    val updatedAt: String,\n    val latestReleaseDate: String? = null,\n    val trendingScore: Double? = null,\n    val popularityScore: Int? = null,\n    val availablePlatforms: List<DiscoveryPlatform> = emptyList(),\n)\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedRepoResponse.kt",
    "content": "package zed.rainxch.home.data.dto\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class CachedRepoResponse(\n    val category: String? = null,\n    val platform: String,\n    val lastUpdated: String,\n    val totalCount: Int,\n    val repositories: List<CachedGithubRepoSummary>,\n)\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt",
    "content": "package zed.rainxch.home.data.mappers\n\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.GithubUser\nimport zed.rainxch.home.data.dto.CachedGithubRepoSummary\n\nfun CachedGithubRepoSummary.toGithubRepoSummary(): GithubRepoSummary =\n    GithubRepoSummary(\n        id = id,\n        name = name,\n        fullName = fullName,\n        owner =\n            GithubUser(\n                id = 0,\n                login = owner.login,\n                avatarUrl = owner.avatarUrl,\n                htmlUrl = \"https://github.com/${owner.login}\",\n            ),\n        description = description,\n        defaultBranch = defaultBranch,\n        htmlUrl = htmlUrl,\n        stargazersCount = stargazersCount,\n        forksCount = forksCount,\n        language = language,\n        topics = topics,\n        releasesUrl = releasesUrl,\n        updatedAt = latestReleaseDate ?: updatedAt,\n        availablePlatforms = availablePlatforms,\n    )\n"
  },
  {
    "path": "feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage zed.rainxch.home.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.emitAll\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withPermit\nimport kotlinx.coroutines.withTimeoutOrNull\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.toLocalDateTime\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport zed.rainxch.core.data.cache.CacheManager\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.HOME_REPOS\nimport zed.rainxch.core.data.dto.GithubRepoNetworkModel\nimport zed.rainxch.core.data.dto.GithubRepoSearchResponse\nimport zed.rainxch.core.data.mappers.toSummary\nimport zed.rainxch.core.data.network.executeRequest\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.home.data.data_source.CachedRepositoriesDataSource\nimport zed.rainxch.home.data.mappers.toGithubRepoSummary\nimport zed.rainxch.home.domain.repository.HomeRepository\nimport kotlin.time.Clock\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.ExperimentalTime\n\nclass HomeRepositoryImpl(\n    private val httpClient: HttpClient,\n    private val devicePlatform: Platform,\n    private val cachedDataSource: CachedRepositoriesDataSource,\n    private val logger: GitHubStoreLogger,\n    private val cacheManager: CacheManager,\n) : HomeRepository {\n    private fun cacheKey(\n        category: String,\n        requestedPlatform: DiscoveryPlatform,\n        page: Int,\n    ): String = \"home:$category:${requestedPlatform.name}:page$page\"\n\n    @OptIn(ExperimentalTime::class)\n    override fun getTrendingRepositories(\n        platform: DiscoveryPlatform,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories> =\n        flow {\n            if (page == 1) {\n                logger.debug(\"Attempting to load cached trending repositories...\")\n\n                val cachedData = cachedDataSource.getCachedTrendingRepos(platform)\n\n                if (cachedData != null && cachedData.repositories.isNotEmpty()) {\n                    logger.debug(\"Using mirror cached data: ${cachedData.repositories.size} repos\")\n\n                    val repos = cachedData.repositories.map { it.toGithubRepoSummary() }\n\n                    val result =\n                        PaginatedDiscoveryRepositories(\n                            repos = repos,\n                            hasMore = false,\n                            nextPageIndex = 2,\n                        )\n                    cacheManager.put(\n                        key =\n                            cacheKey(\n                                category = \"trending\",\n                                requestedPlatform = platform,\n                                page = page,\n                            ),\n                        value = result,\n                        ttlMillis = HOME_REPOS,\n                    )\n                    emit(result)\n                    return@flow\n                } else {\n                    logger.debug(\"No mirror data, checking local cache...\")\n                }\n            }\n\n            val localCached =\n                cacheManager.get<PaginatedDiscoveryRepositories>(\n                    cacheKey(\n                        category = \"trending\",\n                        requestedPlatform = platform,\n                        page = page,\n                    ),\n                )\n            if (localCached != null && localCached.repos.isNotEmpty()) {\n                logger.debug(\"Using locally cached trending repos: ${localCached.repos.size}\")\n                emit(localCached)\n                return@flow\n            }\n\n            val thirtyDaysAgo =\n                Clock.System\n                    .now()\n                    .minus(30.days)\n                    .toLocalDateTime(TimeZone.UTC)\n                    .date\n\n            emitAll(\n                searchReposWithInstallersFlow(\n                    platform = platform,\n                    baseQuery = \"stars:>50 archived:false pushed:>=$thirtyDaysAgo\",\n                    sort = \"stars\",\n                    order = \"desc\",\n                    startPage = page,\n                    category = \"trending\",\n                ),\n            )\n        }.flowOn(Dispatchers.IO)\n\n    @OptIn(ExperimentalTime::class)\n    override fun getHotReleaseRepositories(\n        platform: DiscoveryPlatform,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories> =\n        flow {\n            if (page == 1) {\n                logger.debug(\"Attempting to load cached hot release repositories...\")\n\n                val cachedData = cachedDataSource.getCachedHotReleaseRepos(platform)\n\n                if (cachedData != null && cachedData.repositories.isNotEmpty()) {\n                    logger.debug(\"Using mirror cached data: ${cachedData.repositories.size} repos\")\n\n                    val repos = cachedData.repositories.map { it.toGithubRepoSummary() }\n\n                    val result =\n                        PaginatedDiscoveryRepositories(\n                            repos = repos,\n                            hasMore = false,\n                            nextPageIndex = 2,\n                        )\n                    cacheManager.put(\n                        key =\n                            cacheKey(\n                                category = \"hot_release\",\n                                requestedPlatform = platform,\n                                page = page,\n                            ),\n                        value = result,\n                        ttlMillis = HOME_REPOS,\n                    )\n                    emit(result)\n                    return@flow\n                } else {\n                    logger.debug(\"No mirror data, checking local cache...\")\n                }\n            }\n\n            val localCached =\n                cacheManager.get<PaginatedDiscoveryRepositories>(\n                    cacheKey(\n                        category = \"hot_release\",\n                        requestedPlatform = platform,\n                        page = page,\n                    ),\n                )\n            if (localCached != null && localCached.repos.isNotEmpty()) {\n                logger.debug(\"Using locally cached hot release repos: ${localCached.repos.size}\")\n                emit(localCached)\n                return@flow\n            }\n\n            val fourteenDaysAgo =\n                Clock.System\n                    .now()\n                    .minus(14.days)\n                    .toLocalDateTime(TimeZone.UTC)\n                    .date\n\n            emitAll(\n                searchReposWithInstallersFlow(\n                    platform = platform,\n                    baseQuery = \"stars:>10 archived:false pushed:>=$fourteenDaysAgo\",\n                    sort = \"updated\",\n                    order = \"desc\",\n                    startPage = page,\n                    category = \"hot_release\",\n                ),\n            )\n        }.flowOn(Dispatchers.IO)\n\n    @OptIn(ExperimentalTime::class)\n    override fun getMostPopular(\n        platform: DiscoveryPlatform,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories> =\n        flow {\n            if (page == 1) {\n                logger.debug(\"Attempting to load cached most popular repositories...\")\n\n                val cachedData = cachedDataSource.getCachedMostPopularRepos(platform)\n\n                if (cachedData != null && cachedData.repositories.isNotEmpty()) {\n                    logger.debug(\"Using mirror cached data: ${cachedData.repositories.size} repos\")\n\n                    val repos = cachedData.repositories.map { it.toGithubRepoSummary() }\n\n                    val result =\n                        PaginatedDiscoveryRepositories(\n                            repos = repos,\n                            hasMore = false,\n                            nextPageIndex = 2,\n                        )\n                    cacheManager.put(cacheKey(\"most_popular\", platform, page), result, HOME_REPOS)\n                    emit(result)\n                    return@flow\n                } else {\n                    logger.debug(\"No mirror data, checking local cache...\")\n                }\n            }\n\n            val localCached =\n                cacheManager.get<PaginatedDiscoveryRepositories>(\n                    cacheKey(\n                        category = \"most_popular\",\n                        requestedPlatform = platform,\n                        page = page,\n                    ),\n                )\n            if (localCached != null && localCached.repos.isNotEmpty()) {\n                logger.debug(\"Using locally cached most popular repos: ${localCached.repos.size}\")\n                emit(localCached)\n                return@flow\n            }\n\n            val sixMonthsAgo =\n                Clock.System\n                    .now()\n                    .minus(180.days)\n                    .toLocalDateTime(TimeZone.UTC)\n                    .date\n\n            val oneYearAgo =\n                Clock.System\n                    .now()\n                    .minus(365.days)\n                    .toLocalDateTime(TimeZone.UTC)\n                    .date\n\n            emitAll(\n                searchReposWithInstallersFlow(\n                    platform = platform,\n                    baseQuery = \"stars:>1000 archived:false created:<$sixMonthsAgo pushed:>=$oneYearAgo\",\n                    sort = \"stars\",\n                    order = \"desc\",\n                    startPage = page,\n                    category = \"most_popular\",\n                ),\n            )\n        }.flowOn(Dispatchers.IO)\n\n    private fun searchReposWithInstallersFlow(\n        platform: DiscoveryPlatform,\n        baseQuery: String,\n        sort: String,\n        order: String,\n        startPage: Int,\n        category: String,\n        desiredCount: Int = 10,\n    ): Flow<PaginatedDiscoveryRepositories> =\n        flow {\n            val results = mutableListOf<GithubRepoSummary>()\n            var currentApiPage = startPage\n            val perPage = 100\n            val semaphore = Semaphore(25)\n            val maxPagesToFetch = 5\n            var pagesFetchedCount = 0\n            var lastEmittedCount = 0\n\n            val query = buildSimplifiedQuery(baseQuery, platform)\n            logger.debug(\"Query: $query | Sort: $sort | Page: $startPage\")\n\n            while (results.size < desiredCount && pagesFetchedCount < maxPagesToFetch) {\n                currentCoroutineContext().ensureActive()\n\n                try {\n                    val response =\n                        httpClient\n                            .executeRequest<GithubRepoSearchResponse> {\n                                get(\"/search/repositories\") {\n                                    parameter(\"q\", query)\n                                    parameter(\"sort\", sort)\n                                    parameter(\"order\", order)\n                                    parameter(\"per_page\", perPage)\n                                    parameter(\"page\", currentApiPage)\n                                }\n                            }.getOrElse { error ->\n                                logger.error(\"Search request failed: ${error.message}\")\n                                throw error\n                            }\n\n                    logger.debug(\"API Page $currentApiPage: Got ${response.items.size} repos\")\n\n                    if (response.items.isEmpty()) {\n                        logger.debug(\"No more items from API, breaking\")\n                        break\n                    }\n\n                    val candidates =\n                        response.items\n                            .map { repo -> repo to calculatePlatformScore(repo) }\n                            .filter { it.second > 0 }\n                            .take(50)\n                            .map { it.first }\n\n                    logger.debug(\"Checking ${candidates.size} candidates for installers\")\n\n                    coroutineScope {\n                        val deferredResults =\n                            candidates.map { repo ->\n                                async {\n                                    semaphore.withPermit {\n                                        withTimeoutOrNull(5000) {\n                                            checkRepoHasInstallers(repo)\n                                        }\n                                    }\n                                }\n                            }\n\n                        for (deferred in deferredResults) {\n                            currentCoroutineContext().ensureActive()\n\n                            val result = deferred.await()\n                            if (result != null) {\n                                results.add(result)\n                                logger.debug(\"Found installer repo: ${result.fullName} (${results.size}/$desiredCount)\")\n\n                                if (results.size % 3 == 0 || results.size >= desiredCount) {\n                                    val newItems = results.subList(lastEmittedCount, results.size)\n\n                                    if (newItems.isNotEmpty()) {\n                                        val paginatedResult =\n                                            PaginatedDiscoveryRepositories(\n                                                repos = newItems.toList(),\n                                                hasMore = true,\n                                                nextPageIndex = currentApiPage + 1,\n                                            )\n                                        emit(paginatedResult)\n                                        logger.debug(\"Emitted ${newItems.size} repos (total: ${results.size})\")\n                                        lastEmittedCount = results.size\n                                    }\n                                }\n\n                                if (results.size >= desiredCount) {\n                                    logger.debug(\"Reached desired count, breaking\")\n                                    break\n                                }\n                            }\n                        }\n                    }\n\n                    if (results.size >= desiredCount || response.items.size < perPage) {\n                        logger.debug(\"Breaking: results=${results.size}, response size=${response.items.size}\")\n                        break\n                    }\n\n                    currentApiPage++\n                    pagesFetchedCount++\n                } catch (_: RateLimitException) {\n                    logger.error(\"Rate limited during search\")\n                    break\n                } catch (e: CancellationException) {\n                    throw e\n                } catch (e: Exception) {\n                    logger.error(\"Search failed: ${e.message}\")\n                    e.printStackTrace()\n                    break\n                }\n            }\n\n            if (results.size > lastEmittedCount) {\n                val finalBatch = results.subList(lastEmittedCount, results.size)\n                val finalHasMore =\n                    pagesFetchedCount < maxPagesToFetch && results.size >= desiredCount\n                val finalResult =\n                    PaginatedDiscoveryRepositories(\n                        repos = finalBatch.toList(),\n                        hasMore = finalHasMore,\n                        nextPageIndex = if (finalHasMore) currentApiPage + 1 else currentApiPage,\n                    )\n                emit(finalResult)\n                logger.debug(\"Final emit: ${finalBatch.size} repos (total: ${results.size})\")\n            } else if (results.isEmpty()) {\n                emit(\n                    PaginatedDiscoveryRepositories(\n                        repos = emptyList(),\n                        hasMore = false,\n                        nextPageIndex = currentApiPage,\n                    ),\n                )\n                logger.debug(\"No results found\")\n            }\n\n            if (results.isNotEmpty()) {\n                val allResults =\n                    PaginatedDiscoveryRepositories(\n                        repos = results.toList(),\n                        hasMore = pagesFetchedCount < maxPagesToFetch && results.size >= desiredCount,\n                        nextPageIndex = currentApiPage + 1,\n                    )\n                cacheManager.put(\n                    key =\n                        cacheKey(\n                            category = category,\n                            requestedPlatform = platform,\n                            page = startPage,\n                        ),\n                    value = allResults,\n                    ttlMillis = HOME_REPOS,\n                )\n                logger.debug(\"Cached ${results.size} repos for $category page $startPage\")\n            }\n        }.flowOn(Dispatchers.IO)\n\n    private fun buildSimplifiedQuery(\n        baseQuery: String,\n        requestedPlatform: DiscoveryPlatform,\n    ): String {\n        val topic =\n            when (requestedPlatform) {\n                DiscoveryPlatform.All -> null\n                DiscoveryPlatform.Android -> \"android\"\n                DiscoveryPlatform.Windows -> \"desktop\"\n                DiscoveryPlatform.Macos -> \"macos\"\n                DiscoveryPlatform.Linux -> \"linux\"\n            }\n\n        return if (topic == null) baseQuery else \"$baseQuery topic:$topic\"\n    }\n\n    private fun calculatePlatformScore(repo: GithubRepoNetworkModel): Int {\n        var score = 5\n        val topics = repo.topics.orEmpty().map { it.lowercase() }\n        val language = repo.language?.lowercase()\n        val desc = repo.description?.lowercase() ?: \"\"\n\n        when (devicePlatform) {\n            Platform.ANDROID -> {\n                if (topics.contains(\"android\")) score += 10\n                if (topics.contains(\"mobile\")) score += 5\n                if (language == \"kotlin\" || language == \"java\") score += 5\n                if (desc.contains(\"android\") || desc.contains(\"apk\")) score += 3\n            }\n\n            Platform.WINDOWS, Platform.MACOS, Platform.LINUX -> {\n                if (topics.any {\n                        it in\n                            setOf(\n                                \"desktop\",\n                                \"electron\",\n                                \"app\",\n                                \"gui\",\n                                \"compose-desktop\",\n                            )\n                    }\n                ) {\n                    score += 10\n                }\n                if (topics.contains(\"cross-platform\") || topics.contains(\"multiplatform\")) score += 8\n                if (language in setOf(\"kotlin\", \"c++\", \"rust\", \"c#\", \"swift\", \"dart\")) score += 5\n                if (desc.contains(\"desktop\") || desc.contains(\"application\")) score += 3\n            }\n        }\n\n        return score\n    }\n\n    private suspend fun checkRepoHasInstallers(repo: GithubRepoNetworkModel): GithubRepoSummary? {\n        return try {\n            val allReleases =\n                httpClient\n                    .executeRequest<List<GithubReleaseNetworkModel>> {\n                        get(\"/repos/${repo.owner.login}/${repo.name}/releases\") {\n                            header(\"Accept\", \"application/vnd.github.v3+json\")\n                            parameter(\"per_page\", 10)\n                        }\n                    }.getOrNull() ?: return null\n\n            val stableRelease =\n                allReleases.firstOrNull {\n                    it.draft != true && it.prerelease != true\n                }\n\n            if (stableRelease == null || stableRelease.assets.isEmpty()) {\n                return null\n            }\n\n            val relevantAssets =\n                stableRelease.assets.filter { asset ->\n                    val name = asset.name.lowercase()\n                    when (devicePlatform) {\n                        Platform.ANDROID -> {\n                            name.endsWith(\".apk\")\n                        }\n\n                        Platform.WINDOWS -> {\n                            name.endsWith(\".msi\") || name.endsWith(\".exe\")\n                        }\n\n                        Platform.MACOS -> {\n                            name.endsWith(\".dmg\") || name.endsWith(\".pkg\")\n                        }\n\n                        Platform.LINUX -> {\n                            name.endsWith(\".appimage\") || name.endsWith(\".deb\") ||\n                                name.endsWith(\n                                    \".rpm\",\n                                )\n                        }\n                    }\n                }\n\n            if (relevantAssets.isNotEmpty()) {\n                repo.toSummary()\n            } else {\n                null\n            }\n        } catch (_: Exception) {\n            null\n        }\n    }\n\n    @Serializable\n    private data class GithubReleaseNetworkModel(\n        val assets: List<AssetNetworkModel>,\n        val draft: Boolean? = null,\n        val prerelease: Boolean? = null,\n        @SerialName(\"published_at\") val publishedAt: String? = null,\n    )\n\n    @Serializable\n    private data class AssetNetworkModel(\n        val name: String,\n    )\n}\n"
  },
  {
    "path": "feature/home/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/home/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/home/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/model/HomeCategory.kt",
    "content": "package zed.rainxch.home.domain.model\n\nenum class HomeCategory {\n    TRENDING,\n    HOT_RELEASE,\n    MOST_POPULAR,\n}\n"
  },
  {
    "path": "feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt",
    "content": "package zed.rainxch.home.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories\n\ninterface HomeRepository {\n    fun getTrendingRepositories(\n        platform: DiscoveryPlatform,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories>\n\n    fun getHotReleaseRepositories(\n        platform: DiscoveryPlatform,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories>\n\n    fun getMostPopular(\n        platform: DiscoveryPlatform,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories>\n}\n"
  },
  {
    "path": "feature/home/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/home/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.home.domain)\n\n                implementation(libs.liquid)\n                implementation(libs.kotlinx.collections.immutable)\n\n                implementation(compose.components.resources)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/home/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt",
    "content": "package zed.rainxch.home.presentation\n\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.presentation.model.GithubRepoSummaryUi\nimport zed.rainxch.home.domain.model.HomeCategory\n\nsealed interface HomeAction {\n    data object Refresh : HomeAction\n\n    data object Retry : HomeAction\n\n    data object LoadMore : HomeAction\n\n    data object OnSearchClick : HomeAction\n\n    data object OnSettingsClick : HomeAction\n\n    data object OnAppsClick : HomeAction\n\n    data object OnTogglePlatformPopup : HomeAction\n\n    data class OnShareClick(\n        val repo: GithubRepoSummaryUi,\n    ) : HomeAction\n\n    data class SwitchCategory(\n        val category: HomeCategory,\n    ) : HomeAction\n\n    data class SwitchFilterPlatform(\n        val platform: DiscoveryPlatform,\n    ) : HomeAction\n\n    data class OnRepositoryClick(\n        val repo: GithubRepoSummaryUi,\n    ) : HomeAction\n\n    data class OnRepositoryDeveloperClick(\n        val username: String,\n    ) : HomeAction\n}\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeEvent.kt",
    "content": "package zed.rainxch.home.presentation\n\nsealed interface HomeEvent {\n    data object OnScrollToListTop : HomeEvent\n\n    data class OnMessage(\n        val message: String,\n    ) : HomeEvent\n}\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt",
    "content": "package zed.rainxch.home.presentation\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid\nimport androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells\nimport androidx.compose.foundation.lazy.staggeredgrid.items\nimport androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Done\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport io.github.fletchmckee.liquid.LiquidState\nimport io.github.fletchmckee.liquid.liquefiable\nimport io.github.fletchmckee.liquid.rememberLiquidState\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.core.presentation.components.RepositoryCard\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.core.presentation.utils.toIcons\nimport zed.rainxch.core.presentation.utils.toLabel\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.home.domain.model.HomeCategory\nimport zed.rainxch.home.presentation.components.LiquidGlassCategoryChips\nimport zed.rainxch.home.presentation.locals.LocalHomeTopBarLiquid\n\n@Composable\nfun HomeRoot(\n    onNavigateToSettings: () -> Unit,\n    onNavigateToSearch: () -> Unit,\n    onNavigateToApps: () -> Unit,\n    onNavigateToDetails: (repoId: Long) -> Unit,\n    onNavigateToDeveloperProfile: (username: String) -> Unit,\n    viewModel: HomeViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    val listState = rememberLazyStaggeredGridState()\n    val scope = rememberCoroutineScope()\n    val snackbarHost = remember { SnackbarHostState() }\n\n    ObserveAsEvents(viewModel.events) { event ->\n        when (event) {\n            HomeEvent.OnScrollToListTop -> {\n                scope.launch {\n                    listState.animateScrollToItem(0)\n                }\n            }\n\n            is HomeEvent.OnMessage -> {\n                scope.launch {\n                    snackbarHost.showSnackbar(event.message)\n                }\n            }\n        }\n    }\n\n    HomeScreen(\n        state = state,\n        snackbarHost = snackbarHost,\n        onAction = { action ->\n            when (action) {\n                HomeAction.OnSearchClick -> {\n                    onNavigateToSearch()\n                }\n\n                HomeAction.OnSettingsClick -> {\n                    onNavigateToSettings()\n                }\n\n                HomeAction.OnAppsClick -> {\n                    onNavigateToApps()\n                }\n\n                is HomeAction.OnRepositoryClick -> {\n                    onNavigateToDetails(action.repo.id)\n                }\n\n                is HomeAction.OnRepositoryDeveloperClick -> {\n                    onNavigateToDeveloperProfile(action.username)\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n        listState = listState,\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun HomeScreen(\n    state: HomeState,\n    snackbarHost: SnackbarHostState,\n    onAction: (HomeAction) -> Unit,\n    listState: LazyStaggeredGridState,\n) {\n    val liquidState = LocalBottomNavigationLiquid.current\n    val bottomNavHeight = LocalBottomNavigationHeight.current\n\n    val shouldLoadMore by remember {\n        derivedStateOf {\n            val layoutInfo = listState.layoutInfo\n            val totalItems = layoutInfo.totalItemsCount\n            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()\n\n            totalItems > 0 &&\n                lastVisibleItem != null &&\n                lastVisibleItem.index >= (totalItems - 5) &&\n                !state.isLoadingMore &&\n                !state.isLoading &&\n                state.hasMorePages\n        }\n    }\n\n    val currentOnAction by rememberUpdatedState(onAction)\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) {\n            currentOnAction(HomeAction.LoadMore)\n        }\n    }\n\n    val homeTopbarLiquidState = rememberLiquidState()\n\n    CompositionLocalProvider(\n        LocalHomeTopBarLiquid provides homeTopbarLiquidState,\n    ) {\n        Scaffold(\n            topBar = {\n                TopAppBar(\n                    currentPlatform = state.currentPlatform,\n                    onChangePlatform = {\n                        onAction(HomeAction.SwitchFilterPlatform(it))\n                    },\n                    isPlatformPopupVisible = state.isPlatformPopupVisible,\n                    onTogglePlatformPopup = {\n                        onAction(HomeAction.OnTogglePlatformPopup)\n                    },\n                )\n            },\n            snackbarHost = {\n                SnackbarHost(\n                    hostState = snackbarHost,\n                    modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp),\n                )\n            },\n            containerColor = MaterialTheme.colorScheme.background,\n        ) { innerPadding ->\n            Column(\n                modifier =\n                    Modifier\n                        .fillMaxSize()\n                        .padding(innerPadding)\n                        .padding(horizontal = 8.dp)\n                        .then(\n                            if (state.isLiquidGlassEnabled) {\n                                Modifier\n                                    .liquefiable(liquidState)\n                                    .liquefiable(homeTopbarLiquidState)\n                            } else {\n                                Modifier\n                            },\n                        ),\n            ) {\n                FilterChips(state, onAction)\n\n                Box(Modifier.fillMaxSize()) {\n                    LoadingState(state)\n\n                    ErrorState(state, onAction)\n\n                    MainState(\n                        state = state,\n                        listState = listState,\n                        onAction = onAction,\n                        bottomNavLiquidState = liquidState,\n                        homeTopBarLiquidState = homeTopbarLiquidState,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MainState(\n    state: HomeState,\n    listState: LazyStaggeredGridState,\n    onAction: (HomeAction) -> Unit,\n    bottomNavLiquidState: LiquidState,\n    homeTopBarLiquidState: LiquidState,\n) {\n    val visibleRepos by remember(state.repos, state.isHideSeenEnabled, state.seenRepoIds) {\n        derivedStateOf {\n            if (state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty()) {\n                state.repos.filter { it.repository.id !in state.seenRepoIds }\n            } else {\n                state.repos\n            }\n        }\n    }\n\n    if (visibleRepos.isNotEmpty()) {\n        LazyVerticalStaggeredGrid(\n            state = listState,\n            columns = StaggeredGridCells.Adaptive(350.dp),\n            verticalItemSpacing = 12.dp,\n            horizontalArrangement = Arrangement.spacedBy(12.dp),\n            contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp),\n            modifier = Modifier.fillMaxSize(),\n        ) {\n            items(\n                items = visibleRepos,\n                key = { it.repository.id },\n                contentType = { \"repo\" },\n            ) { discoveryRepository ->\n                RepositoryCard(\n                    discoveryRepositoryUi = discoveryRepository,\n                    onClick = {\n                        onAction(HomeAction.OnRepositoryClick(discoveryRepository.repository))\n                    },\n                    onDeveloperClick = { username ->\n                        onAction(HomeAction.OnRepositoryDeveloperClick(username))\n                    },\n                    onShareClick = {\n                        onAction(HomeAction.OnShareClick(discoveryRepository.repository))\n                    },\n                    modifier =\n                        Modifier\n                            .animateItem()\n                            .then(\n                                if (state.isLiquidGlassEnabled) {\n                                    Modifier\n                                        .liquefiable(bottomNavLiquidState)\n                                        .liquefiable(homeTopBarLiquidState)\n                                } else {\n                                    Modifier\n                                },\n                            ),\n                )\n            }\n\n            if (state.isLoadingMore) {\n                item(key = \"loading_indicator\") {\n                    Box(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(16.dp),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Row(\n                            horizontalArrangement = Arrangement.Center,\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            CircularProgressIndicator(\n                                modifier = Modifier.size(20.dp),\n                            )\n\n                            Spacer(modifier = Modifier.width(8.dp))\n\n                            Text(\n                                text = stringResource(Res.string.home_loading_more),\n                                style = MaterialTheme.typography.titleMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n            }\n\n            if (!state.hasMorePages && !state.isLoadingMore) {\n                item(key = \"end_message\") {\n                    Text(\n                        text = stringResource(Res.string.home_no_more_repositories),\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(16.dp),\n                        textAlign = TextAlign.Center,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        style = MaterialTheme.typography.titleMedium,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun LoadingState(state: HomeState) {\n    if (state.isLoading && state.repos.isEmpty()) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center,\n        ) {\n            Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                CircularWavyProgressIndicator()\n\n                Spacer(modifier = Modifier.height(8.dp))\n\n                Text(\n                    text = stringResource(Res.string.home_finding_repositories),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ErrorState(\n    state: HomeState,\n    onAction: (HomeAction) -> Unit,\n) {\n    if (state.errorMessage != null && state.repos.isEmpty()) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center,\n        ) {\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                modifier = Modifier.padding(16.dp),\n            ) {\n                Text(\n                    text = state.errorMessage,\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n\n                Spacer(modifier = Modifier.height(8.dp))\n\n                GithubStoreButton(\n                    text = stringResource(Res.string.home_retry),\n                    onClick = {\n                        onAction(HomeAction.Retry)\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FilterChips(\n    state: HomeState,\n    onAction: (HomeAction) -> Unit,\n) {\n    LiquidGlassCategoryChips(\n        categories = HomeCategory.entries.toList(),\n        selectedCategory = state.currentCategory,\n        onCategorySelected = { category ->\n            onAction(HomeAction.SwitchCategory(category))\n        },\n        isLiquidGlassEnabled = state.isLiquidGlassEnabled,\n    )\n}\n\n@Composable\n@OptIn(ExperimentalMaterial3Api::class)\nprivate fun TopAppBar(\n    currentPlatform: DiscoveryPlatform,\n    onChangePlatform: (DiscoveryPlatform) -> Unit,\n    isPlatformPopupVisible: Boolean,\n    onTogglePlatformPopup: () -> Unit,\n) {\n    TopAppBar(\n        navigationIcon = {\n            Image(\n                painter = painterResource(Res.drawable.app_icon),\n                contentDescription = null,\n                modifier =\n                    Modifier\n                        .size(48.dp)\n                        .clip(CircleShape)\n                        .background(Color(0xff121212))\n                        .padding(4.dp),\n                contentScale = ContentScale.Crop,\n            )\n        },\n        title = {\n            Text(\n                text = stringResource(Res.string.app_name),\n                style = MaterialTheme.typography.titleLarge,\n                color = MaterialTheme.colorScheme.onBackground,\n                fontWeight = FontWeight.Black,\n                modifier = Modifier.padding(start = 4.dp),\n                maxLines = 2,\n                softWrap = false,\n                overflow = TextOverflow.Ellipsis,\n            )\n        },\n        actions = {\n            val icons = currentPlatform.toIcons()\n\n            Row(\n                modifier =\n                    Modifier\n                        .clip(RoundedCornerShape(16.dp))\n                        .background(MaterialTheme.colorScheme.surfaceContainerHigh)\n                        .clickable(onClick = onTogglePlatformPopup)\n                        .padding(vertical = 4.dp, horizontal = 8.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(2.dp),\n            ) {\n                icons.forEach { icon ->\n                    Icon(\n                        imageVector = icon,\n                        contentDescription = null,\n                        modifier = Modifier.size(18.dp),\n                        tint = MaterialTheme.colorScheme.onSurface,\n                    )\n                }\n            }\n\n            if (isPlatformPopupVisible) {\n                Box {\n                    PlatformsPopup(\n                        onTogglePlatformPopup = onTogglePlatformPopup,\n                        onChangePlatform = onChangePlatform,\n                        currentPlatform = currentPlatform,\n                    )\n                }\n            }\n        },\n        modifier = Modifier.padding(12.dp),\n    )\n}\n\n@Composable\nprivate fun PlatformsPopup(\n    onTogglePlatformPopup: () -> Unit,\n    onChangePlatform: (DiscoveryPlatform) -> Unit,\n    currentPlatform: DiscoveryPlatform,\n) {\n    Popup(\n        onDismissRequest = onTogglePlatformPopup,\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .clip(RoundedCornerShape(8.dp))\n                    .background(MaterialTheme.colorScheme.surfaceContainerHighest)\n                    .padding(6.dp),\n        ) {\n            DiscoveryPlatform.entries.forEach { platform ->\n                Box(\n                    modifier =\n                        Modifier\n                            .clickable(onClick = {\n                                onChangePlatform(platform)\n                                onTogglePlatformPopup()\n                            })\n                            .padding(horizontal = 32.dp, vertical = 8.dp),\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement =\n                            Arrangement.spacedBy(\n                                6.dp,\n                                Alignment.Start,\n                            ),\n                    ) {\n                        if (currentPlatform == platform) {\n                            Icon(\n                                imageVector = Icons.Default.Done,\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.primary,\n                                modifier = Modifier.size(24.dp),\n                            )\n                        }\n\n                        Text(\n                            text = platform.toLabel(),\n                            style = MaterialTheme.typography.titleMedium,\n                            fontWeight = FontWeight.Medium,\n                            color = MaterialTheme.colorScheme.onBackground,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        val liquidState = rememberLiquidState()\n\n        CompositionLocalProvider(\n            value = LocalBottomNavigationLiquid provides liquidState,\n        ) {\n            HomeScreen(\n                state = HomeState(),\n                onAction = {},\n                snackbarHost = SnackbarHostState(),\n                listState = rememberLazyStaggeredGridState(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt",
    "content": "package zed.rainxch.home.presentation\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.InstalledApp\nimport zed.rainxch.core.presentation.model.DiscoveryRepositoryUi\nimport zed.rainxch.home.domain.model.HomeCategory\n\ndata class HomeState(\n    val repos: ImmutableList<DiscoveryRepositoryUi> = persistentListOf(),\n    val installedApps: ImmutableList<InstalledApp> = persistentListOf(),\n    val isLoading: Boolean = false,\n    val isLoadingMore: Boolean = false,\n    val errorMessage: String? = null,\n    val hasMorePages: Boolean = true,\n    val currentCategory: HomeCategory = HomeCategory.TRENDING,\n    val isAppsSectionVisible: Boolean = false,\n    val isUpdateAvailable: Boolean = false,\n    val currentPlatform: DiscoveryPlatform = DiscoveryPlatform.All,\n    val isPlatformPopupVisible: Boolean = false,\n    val isLiquidGlassEnabled: Boolean = true,\n    val isHideSeenEnabled: Boolean = false,\n    val seenRepoIds: Set<Long> = emptySet(),\n)\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt",
    "content": "package zed.rainxch.home.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.collections.immutable.persistentListOf\nimport kotlinx.collections.immutable.toImmutableList\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.SeenReposRepository\nimport zed.rainxch.core.domain.repository.StarredRepository\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.utils.ShareManager\nimport zed.rainxch.core.presentation.model.DiscoveryRepositoryUi\nimport zed.rainxch.core.presentation.utils.toUi\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.home.domain.model.HomeCategory\nimport zed.rainxch.home.domain.repository.HomeRepository\nimport zed.rainxch.home.presentation.HomeEvent.*\n\nclass HomeViewModel(\n    private val homeRepository: HomeRepository,\n    private val installedAppsRepository: InstalledAppsRepository,\n    private val platform: Platform,\n    private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase,\n    private val favouritesRepository: FavouritesRepository,\n    private val starredRepository: StarredRepository,\n    private val logger: GitHubStoreLogger,\n    private val shareManager: ShareManager,\n    private val tweaksRepository: TweaksRepository,\n    private val seenReposRepository: SeenReposRepository,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n    private var currentJob: Job? = null\n    private var switchCategoryJob: Job? = null\n    private var nextPageIndex = 1\n\n    private val _state = MutableStateFlow(HomeState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    syncSystemState()\n\n                    loadPlatform()\n                    loadRepos(isInitial = true)\n                    observeInstalledApps()\n                    observeFavourites()\n                    observeStarredRepos()\n                    observeLiquidGlassEnabled()\n                    observeSeenRepos()\n                    observeHideSeenEnabled()\n\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = HomeState(),\n            )\n\n    private val _events = Channel<HomeEvent>()\n    val events = _events.receiveAsFlow()\n\n    private fun syncSystemState() {\n        viewModelScope.launch {\n            try {\n                val result = syncInstalledAppsUseCase()\n                if (result.isFailure) {\n                    logger.warn(\"Initial sync had issues: ${result.exceptionOrNull()?.message}\")\n                }\n            } catch (e: Exception) {\n                logger.error(\"Initial sync failed: ${e.message}\")\n            }\n        }\n    }\n\n    private fun loadPlatform() {\n        _state.update {\n            it.copy(isAppsSectionVisible = platform == Platform.ANDROID)\n        }\n    }\n\n    private fun observeInstalledApps() {\n        viewModelScope.launch {\n            installedAppsRepository.getAllInstalledApps().collect { installedApps ->\n                val installedMap = installedApps.associateBy { it.repoId }\n                _state.update { current ->\n                    current.copy(\n                        repos =\n                            current.repos\n                                .map { homeRepo ->\n                                    val app = installedMap[homeRepo.repository.id]\n                                    homeRepo.copy(\n                                        isInstalled = app != null,\n                                        isUpdateAvailable = app?.isUpdateAvailable ?: false,\n                                    )\n                                }.toImmutableList(),\n                        isUpdateAvailable = installedMap.any { it.value.isUpdateAvailable },\n                    )\n                }\n            }\n        }\n    }\n\n    private fun loadRepos(\n        isInitial: Boolean = false,\n        category: HomeCategory? = null,\n        platform: DiscoveryPlatform? = null,\n    ): Job? {\n        currentJob?.cancel()\n\n        if (_state.value.isLoading || _state.value.isLoadingMore) {\n            logger.debug(\"Already loading, skipping...\")\n            return null\n        }\n\n        if (isInitial) {\n            nextPageIndex = 1\n        }\n\n        val targetCategory = category ?: _state.value.currentCategory\n        val targetPlatform = platform ?: _state.value.currentPlatform\n\n        logger.debug(\"Loading repos: category=$targetCategory, page=$nextPageIndex, isInitial=$isInitial\")\n\n        return viewModelScope\n            .launch {\n                _state.update {\n                    it.copy(\n                        isLoading = isInitial,\n                        isLoadingMore = !isInitial,\n                        errorMessage = null,\n                        currentCategory = targetCategory,\n                        currentPlatform = targetPlatform,\n                        repos = if (isInitial) persistentListOf() else it.repos,\n                    )\n                }\n\n                try {\n                    val flow =\n                        when (targetCategory) {\n                            HomeCategory.TRENDING -> {\n                                homeRepository.getTrendingRepositories(\n                                    platform = targetPlatform,\n                                    page = nextPageIndex,\n                                )\n                            }\n\n                            HomeCategory.HOT_RELEASE -> {\n                                homeRepository.getHotReleaseRepositories(\n                                    platform = targetPlatform,\n                                    page = nextPageIndex,\n                                )\n                            }\n\n                            HomeCategory.MOST_POPULAR -> {\n                                homeRepository.getMostPopular(\n                                    platform = targetPlatform,\n                                    page = nextPageIndex,\n                                )\n                            }\n                        }\n\n                    flow.collect { paginatedRepos ->\n                        logger.debug(\n                            \"Received ${paginatedRepos.repos.size} repos, hasMore=${paginatedRepos.hasMore}, nextPage=${paginatedRepos.nextPageIndex}\",\n                        )\n\n                        this@HomeViewModel.nextPageIndex = paginatedRepos.nextPageIndex\n\n                        val installedAppsMap =\n                            installedAppsRepository\n                                .getAllInstalledApps()\n                                .first()\n                                .associateBy { it.repoId }\n\n                        val favoritesMap =\n                            favouritesRepository\n                                .getAllFavorites()\n                                .first()\n                                .associateBy { it.repoId }\n\n                        val starredReposMap =\n                            starredRepository\n                                .getAllStarred()\n                                .first()\n                                .associateBy { it.repoId }\n\n                        val seenIds = _state.value.seenRepoIds\n\n                        val newReposWithStatus =\n                            paginatedRepos.repos.map { repo ->\n                                val app = installedAppsMap[repo.id]\n                                val favourite = favoritesMap[repo.id]\n                                val starred = starredReposMap[repo.id]\n\n                                DiscoveryRepositoryUi(\n                                    isInstalled = app != null,\n                                    isFavourite = favourite != null,\n                                    isStarred = starred != null,\n                                    isSeen = repo.id in seenIds,\n                                    isUpdateAvailable = app?.isUpdateAvailable ?: false,\n                                    repository = repo.toUi(),\n                                )\n                            }\n\n                        _state.update { currentState ->\n                            val rawList = currentState.repos + newReposWithStatus\n                            val uniqueList = rawList.distinctBy { it.repository.fullName }\n\n                            currentState.copy(\n                                repos = uniqueList.toImmutableList(),\n                                hasMorePages = paginatedRepos.hasMore,\n                                errorMessage =\n                                    if (uniqueList.isEmpty() && !paginatedRepos.hasMore) {\n                                        getString(Res.string.no_repositories_found)\n                                    } else {\n                                        null\n                                    },\n                            )\n                        }\n                    }\n\n                    logger.debug(\"Flow completed\")\n                    _state.update {\n                        it.copy(isLoading = false, isLoadingMore = false)\n                    }\n                } catch (t: Throwable) {\n                    if (t is CancellationException) {\n                        logger.debug(\"Load cancelled (expected)\")\n                        throw t\n                    }\n\n                    logger.error(\"Load failed: ${t.message}\")\n                    _state.update {\n                        it.copy(\n                            isLoading = false,\n                            isLoadingMore = false,\n                            errorMessage =\n                                t.message\n                                    ?: getString(Res.string.home_failed_to_load_repositories),\n                        )\n                    }\n                }\n            }.also {\n                currentJob = it\n            }\n    }\n\n    fun onAction(action: HomeAction) {\n        when (action) {\n            HomeAction.Refresh -> {\n                viewModelScope.launch {\n                    syncInstalledAppsUseCase()\n                    nextPageIndex = 1\n                    loadRepos(isInitial = true)\n                }\n            }\n\n            HomeAction.Retry -> {\n                nextPageIndex = 1\n                loadRepos(isInitial = true)\n            }\n\n            HomeAction.LoadMore -> {\n                logger.debug(\n                    \"LoadMore action: isLoading=${_state.value.isLoading}, isLoadingMore=${_state.value.isLoadingMore}, hasMore=${_state.value.hasMorePages}\",\n                )\n\n                if (!_state.value.isLoadingMore && !_state.value.isLoading && _state.value.hasMorePages) {\n                    loadRepos(isInitial = false)\n                }\n            }\n\n            is HomeAction.SwitchCategory -> {\n                if (_state.value.currentCategory != action.category) {\n                    nextPageIndex = 1\n                    switchCategoryJob?.cancel()\n                    switchCategoryJob =\n                        viewModelScope.launch {\n                            loadRepos(isInitial = true, category = action.category)?.join()\n                                ?: return@launch\n                            _events.send(HomeEvent.OnScrollToListTop)\n                        }\n                }\n            }\n\n            is HomeAction.OnShareClick -> {\n                viewModelScope.launch {\n                    runCatching {\n                        shareManager.shareText(\"https://github-store.org/app?repo=${action.repo.fullName}\")\n                    }.onFailure { t ->\n                        logger.error(\"Failed to share link: ${t.message}\")\n                        _events.send(\n                            OnMessage(getString(Res.string.failed_to_share_link)),\n                        )\n                        return@launch\n                    }\n\n                    if (platform != Platform.ANDROID) {\n                        _events.send(OnMessage(getString(Res.string.link_copied_to_clipboard)))\n                    }\n                }\n            }\n\n            is HomeAction.SwitchFilterPlatform -> {\n                if (_state.value.currentPlatform != action.platform) {\n                    nextPageIndex = 1\n                    switchCategoryJob?.cancel()\n                    switchCategoryJob =\n                        viewModelScope.launch {\n                            loadRepos(isInitial = true, platform = action.platform)?.join()\n                                ?: return@launch\n                            _events.send(HomeEvent.OnScrollToListTop)\n                        }\n                }\n            }\n\n            HomeAction.OnTogglePlatformPopup -> {\n                _state.update {\n                    it.copy(\n                        isPlatformPopupVisible = !it.isPlatformPopupVisible,\n                    )\n                }\n            }\n\n            is HomeAction.OnRepositoryClick -> {\n                // Handled in composable\n            }\n\n            is HomeAction.OnRepositoryDeveloperClick -> {\n                // Handled in composable\n            }\n\n            HomeAction.OnSearchClick -> {\n                // Handled in composable\n            }\n\n            HomeAction.OnSettingsClick -> {\n                // Handled in composable\n            }\n\n            HomeAction.OnAppsClick -> {\n                // Handled in composable\n            }\n        }\n    }\n\n    private fun observeLiquidGlassEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getLiquidGlassEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(isLiquidGlassEnabled = enabled)\n                }\n            }\n        }\n    }\n\n    private fun observeSeenRepos() {\n        viewModelScope.launch {\n            seenReposRepository.getAllSeenRepoIds().collect { ids ->\n                _state.update { current ->\n                    current.copy(\n                        seenRepoIds = ids,\n                        repos =\n                            current.repos\n                                .map { repo ->\n                                    repo.copy(isSeen = repo.repository.id in ids)\n                                }.toImmutableList(),\n                    )\n                }\n            }\n        }\n    }\n\n    private fun observeHideSeenEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getHideSeenEnabled().collect { enabled ->\n                _state.update { it.copy(isHideSeenEnabled = enabled) }\n            }\n        }\n    }\n\n    private fun observeFavourites() {\n        viewModelScope.launch {\n            favouritesRepository.getAllFavorites().collect { favourites ->\n                val favouritesMap = favourites.associateBy { it.repoId }\n                _state.update { current ->\n                    current.copy(\n                        repos =\n                            current.repos\n                                .map { homeRepo ->\n                                    homeRepo.copy(\n                                        isFavourite = favouritesMap.containsKey(homeRepo.repository.id),\n                                    )\n                                }.toImmutableList(),\n                    )\n                }\n            }\n        }\n    }\n\n    private fun observeStarredRepos() {\n        viewModelScope.launch {\n            starredRepository.getAllStarred().collect { starredRepos ->\n                val starredReposById = starredRepos.associateBy { it.repoId }\n                _state.update { current ->\n                    current.copy(\n                        repos =\n                            current.repos\n                                .map { homeRepo ->\n                                    homeRepo.copy(\n                                        isStarred = starredReposById.containsKey(homeRepo.repository.id),\n                                    )\n                                }.toImmutableList(),\n                    )\n                }\n            }\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        currentJob?.cancel()\n    }\n}\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeFilterChips.kt",
    "content": "package zed.rainxch.home.presentation.components\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsPressedAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.luminance\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInParent\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport io.github.fletchmckee.liquid.liquid\nimport kotlinx.coroutines.launch\nimport zed.rainxch.core.presentation.utils.isLiquidFrostAvailable\nimport zed.rainxch.home.domain.model.HomeCategory\nimport zed.rainxch.home.presentation.locals.LocalHomeTopBarLiquid\nimport zed.rainxch.home.presentation.utils.displayText\n\n@Composable\nfun LiquidGlassCategoryChips(\n    categories: List<HomeCategory>,\n    selectedCategory: HomeCategory,\n    onCategorySelected: (HomeCategory) -> Unit,\n    isLiquidGlassEnabled: Boolean = true,\n    modifier: Modifier = Modifier,\n) {\n    val liquidState = LocalHomeTopBarLiquid.current\n    val density = LocalDensity.current\n\n    val isDarkTheme =\n        !MaterialTheme.colorScheme.background\n            .luminance()\n            .let { it > 0.5f }\n\n    val itemPositions = remember { mutableMapOf<Int, Pair<Float, Float>>() }\n    var selectedItemPos by remember { mutableStateOf<Pair<Float, Float>?>(null) }\n\n    val selectedIndex = categories.indexOf(selectedCategory)\n\n    val rowPaddingDp = 6.dp\n    val rowPaddingPx = with(density) { rowPaddingDp.toPx() }\n    val insetPx = with(density) { 2.dp.toPx() }\n\n    val indicatorX = remember { Animatable(0f) }\n    val indicatorWidth = remember { Animatable(0f) }\n\n    LaunchedEffect(selectedIndex, selectedItemPos) {\n        val raw = selectedItemPos ?: itemPositions[selectedIndex] ?: return@LaunchedEffect\n        val targetX = raw.first + rowPaddingPx - insetPx\n        val targetW = raw.second + insetPx * 2f\n\n        launch {\n            indicatorX.animateTo(\n                targetValue = targetX,\n                animationSpec =\n                    spring(\n                        dampingRatio = Spring.DampingRatioLowBouncy,\n                        stiffness = Spring.StiffnessLow,\n                    ),\n            )\n        }\n        launch {\n            indicatorWidth.animateTo(\n                targetValue = targetW,\n                animationSpec =\n                    spring(\n                        dampingRatio = Spring.DampingRatioNoBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n            )\n        }\n    }\n\n    val glassHighColor =\n        if (isDarkTheme) Color.White.copy(alpha = .14f) else Color.White.copy(alpha = .50f)\n    val glassLowColor =\n        if (isDarkTheme) Color.White.copy(alpha = .05f) else Color.White.copy(alpha = .18f)\n    val specularColor =\n        if (isDarkTheme) Color.White.copy(alpha = .20f) else Color.White.copy(alpha = .55f)\n    val innerGlowColor =\n        if (isDarkTheme) Color.White.copy(alpha = .04f) else Color.White.copy(alpha = .10f)\n    val borderColor = if (isDarkTheme) Color.White.copy(alpha = .10f) else Color.Transparent\n\n    val containerShape = RoundedCornerShape(20.dp)\n    val useLiquid = isLiquidGlassEnabled && isLiquidFrostAvailable()\n\n    Box(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .clip(containerShape)\n                .then(\n                    if (useLiquid) {\n                        Modifier\n                            .background(\n                                if (isDarkTheme) {\n                                    MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = .30f)\n                                } else {\n                                    MaterialTheme.colorScheme.primaryContainer.copy(alpha = .45f)\n                                },\n                            ).liquid(liquidState) {\n                                this.shape = containerShape\n                                this.frost = if (isDarkTheme) 14.dp else 12.dp\n                                this.curve = if (isDarkTheme) .30f else .40f\n                                this.refraction = if (isDarkTheme) .06f else .10f\n                                this.dispersion = if (isDarkTheme) .15f else .22f\n                                this.saturation = if (isDarkTheme) .35f else .50f\n                                this.contrast = if (isDarkTheme) 1.7f else 1.5f\n                            }\n                    } else {\n                        Modifier\n                            .background(MaterialTheme.colorScheme.surfaceContainer)\n                            .border(\n                                width = 1.dp,\n                                color = MaterialTheme.colorScheme.outlineVariant,\n                                shape = containerShape,\n                            )\n                    },\n                ),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .matchParentSize()\n                    .drawBehind {\n                        if (indicatorWidth.value > 0f) {\n                            val pillTop = 5.dp.toPx()\n                            val pillHeight = size.height - 10.dp.toPx()\n                            val pillCorner = 14.dp.toPx()\n                            val pillRadius = CornerRadius(pillCorner)\n\n                            if (isDarkTheme) {\n                                drawRoundRect(\n                                    color = borderColor,\n                                    topLeft =\n                                        Offset(\n                                            indicatorX.value - .5.dp.toPx(),\n                                            pillTop - .5.dp.toPx(),\n                                        ),\n                                    size =\n                                        Size(\n                                            indicatorWidth.value + 1.dp.toPx(),\n                                            pillHeight + 1.dp.toPx(),\n                                        ),\n                                    cornerRadius = pillRadius,\n                                    style = Stroke(width = 1.dp.toPx()),\n                                )\n                            }\n\n                            drawRoundRect(\n                                brush =\n                                    Brush.verticalGradient(\n                                        colors = listOf(glassHighColor, glassLowColor),\n                                        startY = pillTop,\n                                        endY = pillTop + pillHeight,\n                                    ),\n                                topLeft = Offset(indicatorX.value, pillTop),\n                                size = Size(indicatorWidth.value, pillHeight),\n                                cornerRadius = pillRadius,\n                            )\n\n                            val specLeft = indicatorX.value + indicatorWidth.value * .12f\n                            val specWidth = indicatorWidth.value * .76f\n                            drawRoundRect(\n                                brush =\n                                    Brush.horizontalGradient(\n                                        colors =\n                                            listOf(\n                                                Color.Transparent,\n                                                specularColor,\n                                                specularColor.copy(alpha = specularColor.alpha * .6f),\n                                                Color.Transparent,\n                                            ),\n                                        startX = specLeft,\n                                        endX = specLeft + specWidth,\n                                    ),\n                                topLeft = Offset(specLeft, pillTop + 1.dp.toPx()),\n                                size = Size(specWidth, 1.5.dp.toPx()),\n                                cornerRadius = CornerRadius(1.dp.toPx()),\n                            )\n\n                            drawRoundRect(\n                                brush =\n                                    Brush.verticalGradient(\n                                        colors = listOf(Color.Transparent, innerGlowColor),\n                                        startY = pillTop + pillHeight - 6.dp.toPx(),\n                                        endY = pillTop + pillHeight,\n                                    ),\n                                topLeft =\n                                    Offset(\n                                        indicatorX.value + 6.dp.toPx(),\n                                        pillTop + pillHeight - 5.dp.toPx(),\n                                    ),\n                                size = Size(indicatorWidth.value - 12.dp.toPx(), 4.dp.toPx()),\n                                cornerRadius = CornerRadius(2.dp.toPx()),\n                            )\n\n                            val edgeAlpha = if (isDarkTheme) .06f else .12f\n                            drawRoundRect(\n                                brush =\n                                    Brush.horizontalGradient(\n                                        colors =\n                                            listOf(\n                                                Color.White.copy(alpha = edgeAlpha),\n                                                Color.Transparent,\n                                            ),\n                                        startX = indicatorX.value,\n                                        endX = indicatorX.value + 4.dp.toPx(),\n                                    ),\n                                topLeft = Offset(indicatorX.value, pillTop + 4.dp.toPx()),\n                                size = Size(3.dp.toPx(), pillHeight - 8.dp.toPx()),\n                                cornerRadius = CornerRadius(1.5.dp.toPx()),\n                            )\n                            drawRoundRect(\n                                brush =\n                                    Brush.horizontalGradient(\n                                        colors =\n                                            listOf(\n                                                Color.Transparent,\n                                                Color.White.copy(alpha = edgeAlpha),\n                                            ),\n                                        startX = indicatorX.value + indicatorWidth.value - 4.dp.toPx(),\n                                        endX = indicatorX.value + indicatorWidth.value,\n                                    ),\n                                topLeft =\n                                    Offset(\n                                        indicatorX.value + indicatorWidth.value - 3.dp.toPx(),\n                                        pillTop + 4.dp.toPx(),\n                                    ),\n                                size = Size(3.dp.toPx(), pillHeight - 8.dp.toPx()),\n                                cornerRadius = CornerRadius(1.5.dp.toPx()),\n                            )\n                        }\n                    },\n        )\n\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = rowPaddingDp, vertical = 3.dp),\n            horizontalArrangement = Arrangement.spacedBy(0.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            categories.forEachIndexed { index, category ->\n                LiquidGlassCategoryChip(\n                    category = category,\n                    isSelected = category == selectedCategory,\n                    onSelect = { onCategorySelected(category) },\n                    modifier = Modifier.weight(1f),\n                    onPositioned = { x, width ->\n                        itemPositions[index] = x to width\n                        if (index == selectedIndex) {\n                            selectedItemPos = x to width\n                        }\n                        if (index == selectedIndex && indicatorWidth.value == 0f) {\n                            indicatorX.snapTo(x + rowPaddingPx - insetPx)\n                            indicatorWidth.snapTo(width + insetPx * 2f)\n                        }\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LiquidGlassCategoryChip(\n    category: HomeCategory,\n    isSelected: Boolean,\n    onSelect: () -> Unit,\n    modifier: Modifier = Modifier,\n    onPositioned: suspend (x: Float, width: Float) -> Unit,\n) {\n    val scope = rememberCoroutineScope()\n    val interactionSource = remember { MutableInteractionSource() }\n    val isPressed by interactionSource.collectIsPressedAsState()\n\n    val pressScale by animateFloatAsState(\n        targetValue = if (isPressed) 0.90f else 1f,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessMedium,\n            ),\n        label = \"chipPressScale\",\n    )\n\n    val selectedAlpha by animateFloatAsState(\n        targetValue = if (isSelected) 1f else 0f,\n        animationSpec = tween(200),\n        label = \"selectedAlpha\",\n    )\n\n    val textColor by animateColorAsState(\n        targetValue =\n            if (isSelected) {\n                MaterialTheme.colorScheme.onSurface\n            } else {\n                MaterialTheme.colorScheme.onSurface.copy(alpha = .65f)\n            },\n        animationSpec = tween(250),\n        label = \"chipTextColor\",\n    )\n\n    Box(\n        modifier =\n            modifier\n                .clip(CircleShape)\n                .clickable(\n                    interactionSource = interactionSource,\n                    indication = null,\n                ) { onSelect() }\n                .onGloballyPositioned { coordinates ->\n                    val x = coordinates.positionInParent().x\n                    val width = coordinates.size.width.toFloat()\n                    scope.launch { onPositioned(x, width) }\n                }.graphicsLayer {\n                    scaleX = pressScale\n                    scaleY = pressScale\n                }.padding(vertical = 8.dp),\n        contentAlignment = Alignment.Center,\n    ) {\n        Box(contentAlignment = Alignment.Center) {\n            Text(\n                text = category.displayText(),\n                style =\n                    MaterialTheme.typography.labelLarge.copy(\n                        fontWeight = FontWeight.Medium,\n                    ),\n                color = textColor,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                textAlign = TextAlign.Center,\n                modifier = Modifier.graphicsLayer { alpha = 1f - selectedAlpha },\n            )\n            Text(\n                text = category.displayText(),\n                style =\n                    MaterialTheme.typography.labelLarge.copy(\n                        fontWeight = FontWeight.Bold,\n                    ),\n                color = textColor,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                textAlign = TextAlign.Center,\n                modifier = Modifier.graphicsLayer { alpha = selectedAlpha },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/locals/LocalHomeTopBarLiquid.kt",
    "content": "package zed.rainxch.home.presentation.locals\n\nimport androidx.compose.runtime.compositionLocalOf\nimport io.github.fletchmckee.liquid.LiquidState\n\nval LocalHomeTopBarLiquid =\n    compositionLocalOf<LiquidState> {\n        error(\"State isn't initialized!?\")\n    }\n"
  },
  {
    "path": "feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/utils/HomeCategoryMapper.kt",
    "content": "package zed.rainxch.home.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.home.domain.model.HomeCategory\nimport zed.rainxch.home.domain.model.HomeCategory.*\n\n@Composable\nfun HomeCategory.displayText(): String =\n    when (this) {\n        TRENDING -> stringResource(Res.string.home_category_trending)\n        HOT_RELEASE -> stringResource(Res.string.home_category_hot_release)\n        MOST_POPULAR -> stringResource(Res.string.home_category_most_popular)\n    }\n"
  },
  {
    "path": "feature/profile/CLAUDE.md",
    "content": "# CLAUDE.md - Profile Feature\n\n## Purpose\n\nUser profile screen combining account management, appearance settings, network proxy configuration, installer method selection (including Shizuku silent install on Android), and sponsor/about info. Replaces the former `feature/settings/` module. Accessible from the bottom navigation bar.\n\n## Module Structure\n\n```\nfeature/profile/\n├── domain/\n│   ├── model/UserProfile.kt          # User profile data model\n│   └── repository/ProfileRepository.kt  # Auth state, user, logout, cache\n├── data/\n│   ├── di/SharedModule.kt            # Koin: profileModule\n│   ├── repository/ProfileRepositoryImpl.kt  # Implementation\n│   └── mappers/UserProfileMappers.kt # DTO → domain model mappers\n└── presentation/\n    ├── ProfileViewModel.kt            # State management for profile screen\n    ├── ProfileState.kt                # User, theme, proxy, installer, Shizuku status\n    ├── ProfileAction.kt               # Theme, logout, proxy, installer, Shizuku actions\n    ├── ProfileEvent.kt                # One-off events (navigation, etc.)\n    ├── ProfileRoot.kt                 # Main composable (LazyColumn of sections)\n    ├── SponsorScreen.kt               # Sponsor/donation screen\n    ├── model/ProxyType.kt             # NONE, HTTP, SOCKS\n    └── components/\n        ├── LogoutDialog.kt            # Logout confirmation dialog\n        ├── SectionText.kt             # Section header text component\n        └── sections/\n            ├── Account.kt             # Login/logout actions\n            ├── AccountSection.kt      # Account info display\n            ├── Appearance.kt          # Theme color, font, dark mode, AMOLED\n            ├── Installation.kt        # Installer type selector (Default/Shizuku) with status\n            ├── Network.kt             # Proxy configuration (type, host, port, auth)\n            ├── Options.kt             # Favourites, starred, clipboard detection\n            ├── Others.kt              # Help, clear cache, version info\n            ├── ProfileSection.kt      # User avatar, name, bio\n            └── SettingsSection.kt     # Settings group container\n```\n\n## Key Interfaces\n\n```kotlin\ninterface ProfileRepository {\n    val isUserLoggedIn: Flow<Boolean>\n    fun getUser(): Flow<UserProfile?>\n    fun getVersionName(): String\n    suspend fun logout()\n    fun observeCacheSize(): Flow<Long>\n    suspend fun clearCache()\n}\n```\n\n## State\n\n```kotlin\ndata class ProfileState(\n    val userProfile: UserProfile?,\n    val selectedThemeColor: AppTheme,\n    val selectedFontTheme: FontTheme,\n    val isLogoutDialogVisible: Boolean,\n    val isUserLoggedIn: Boolean,\n    val isAmoledThemeEnabled: Boolean,\n    val isDarkTheme: Boolean?,\n    val versionName: String,\n    val proxyType: ProxyType,\n    val proxyHost: String, val proxyPort: String,\n    val proxyUsername: String, val proxyPassword: String,\n    val isProxyPasswordVisible: Boolean,\n    val autoDetectClipboardLinks: Boolean,\n    val cacheSize: String,\n    val installerType: InstallerType,          // DEFAULT or SHIZUKU\n    val shizukuAvailability: ShizukuAvailability  // UNAVAILABLE, NOT_RUNNING, PERMISSION_NEEDED, READY\n)\n```\n\n## Navigation\n\nRoutes:\n- `GithubStoreGraph.ProfileScreen` (data object, no params) — main profile screen\n- `GithubStoreGraph.SponsorScreen` (data object, no params) — sponsor/donation page\n\n## Implementation Notes\n\n- **Installation section** (Android only): Radio-button group to choose between Default (standard system dialog) and Shizuku (silent install). Uses `selectableGroup` + `selectable` with `Role.RadioButton` for accessibility.\n- **Shizuku status**: Observes `InstallerStatusProvider.shizukuAvailability` flow to show real-time status (not installed, not running, permission needed, ready). Grant permission button calls `InstallerStatusProvider.requestShizukuPermission()`.\n- **Installer preference** stored via `ThemesRepository.setInstallerType()` / `getInstallerType()` (persisted in DataStore).\n- **Proxy settings**: Supports HTTP and SOCKS proxies with optional authentication. Saved via `ProxyRepository` from core/domain.\n- **Appearance**: Theme color (`AppTheme` enum), font (`FontTheme`), dark mode toggle, AMOLED black toggle.\n- **Account**: Shows GitHub user profile when logged in; login/logout with confirmation dialog.\n- **Cache management**: Displays cache size and allows clearing.\n- **BuildKonfig**: Uses `convention.buildkonfig` plugin for build-time configuration.\n- ViewModel depends on: `ProfileRepository`, `ThemesRepository`, `ProxyRepository`, `InstallerStatusProvider`, `Platform`\n"
  },
  {
    "path": "feature/profile/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/profile/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.profile.domain)\n\n                implementation(libs.bundles.koin.common)\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt",
    "content": "package zed.rainxch.profile.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.profile.data.repository.ProfileRepositoryImpl\nimport zed.rainxch.profile.domain.repository.ProfileRepository\n\nval settingsModule =\n    module {\n        single<ProfileRepository> {\n            ProfileRepositoryImpl(\n                authenticationState = get(),\n                tokenStore = get(),\n                httpClient = get(),\n                cacheManager = get(),\n                logger = get(),\n                fileLocationsProvider = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/mappers/UserProfileMappers.kt",
    "content": "package zed.rainxch.profile.data.mappers\n\nimport zed.rainxch.core.data.dto.UserProfileNetwork\nimport zed.rainxch.profile.domain.model.UserProfile\n\nfun UserProfileNetwork.toUserProfile(): UserProfile =\n    UserProfile(\n        id = id.toInt(),\n        imageUrl = avatarUrl,\n        name = name ?: login,\n        username = login,\n        bio = bio,\n        repositoryCount = publicRepos,\n        followers = followers,\n        following = following,\n    )\n"
  },
  {
    "path": "feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt",
    "content": "package zed.rainxch.profile.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.http.HttpHeaders\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.flowOn\nimport zed.rainxch.core.data.cache.CacheManager\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.USER_PROFILE\nimport zed.rainxch.core.data.data_source.TokenStore\nimport zed.rainxch.core.data.dto.UserProfileNetwork\nimport zed.rainxch.core.data.network.executeRequest\nimport zed.rainxch.core.data.services.FileLocationsProvider\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.repository.AuthenticationState\nimport zed.rainxch.feature.profile.data.BuildKonfig\nimport zed.rainxch.profile.data.mappers.toUserProfile\nimport zed.rainxch.profile.domain.model.UserProfile\nimport zed.rainxch.profile.domain.repository.ProfileRepository\n\nclass ProfileRepositoryImpl(\n    private val authenticationState: AuthenticationState,\n    private val tokenStore: TokenStore,\n    private val httpClient: HttpClient,\n    private val cacheManager: CacheManager,\n    private val logger: GitHubStoreLogger,\n    private val fileLocationsProvider: FileLocationsProvider,\n) : ProfileRepository {\n    companion object {\n        private const val CACHE_KEY = \"profile:me\"\n    }\n\n    override val isUserLoggedIn: Flow<Boolean>\n        get() =\n            authenticationState\n                .isUserLoggedIn()\n                .flowOn(Dispatchers.IO)\n\n    override fun getUser(): Flow<UserProfile?> =\n        flow {\n            val token = tokenStore.currentToken()\n            if (token == null) {\n                cacheManager.invalidate(CACHE_KEY)\n                emit(null)\n                return@flow\n            }\n\n            val cached = cacheManager.get<UserProfile>(CACHE_KEY)\n            if (cached != null) {\n                logger.debug(\"Profile cache hit\")\n                emit(cached)\n                return@flow\n            }\n\n            try {\n                val networkProfile =\n                    httpClient\n                        .executeRequest<UserProfileNetwork> {\n                            get(\"/user\") {\n                                header(HttpHeaders.Accept, \"application/vnd.github+json\")\n                            }\n                        }.getOrThrow()\n\n                val userProfile = networkProfile.toUserProfile()\n                cacheManager.put(CACHE_KEY, userProfile, USER_PROFILE)\n                logger.debug(\"Fetched and cached user profile: ${userProfile.username}\")\n                emit(userProfile)\n            } catch (e: Exception) {\n                logger.error(\"Failed to fetch user profile: ${e.message}\")\n\n                val stale = cacheManager.getStale<UserProfile>(CACHE_KEY)\n                if (stale != null) {\n                    logger.debug(\"Using stale cached profile as fallback\")\n                    emit(stale)\n                } else {\n                    emit(null)\n                }\n            }\n        }.flowOn(Dispatchers.IO)\n\n    override fun getVersionName(): String = BuildKonfig.VERSION_NAME\n\n    override suspend fun logout() {\n        tokenStore.clear()\n        cacheManager.clearAll()\n    }\n\n    override fun observeCacheSize(): Flow<Long> =\n        flow {\n            val sizeBytes = fileLocationsProvider.getCacheSizeBytes()\n            emit(sizeBytes)\n        }.flowOn(Dispatchers.IO)\n\n    override suspend fun clearCache() {\n        fileLocationsProvider.clearCacheFiles()\n        cacheManager.clearAll()\n        logger.debug(\"Cache cleared successfully\")\n    }\n}\n"
  },
  {
    "path": "feature/profile/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/profile/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/model/UserProfile.kt",
    "content": "package zed.rainxch.profile.domain.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UserProfile(\n    val id: Int,\n    val imageUrl: String,\n    val name: String,\n    val username: String,\n    val bio: String?,\n    val repositoryCount: Int,\n    val followers: Int,\n    val following: Int,\n)\n"
  },
  {
    "path": "feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt",
    "content": "package zed.rainxch.profile.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.profile.domain.model.UserProfile\n\ninterface ProfileRepository {\n    val isUserLoggedIn: Flow<Boolean>\n\n    fun getUser(): Flow<UserProfile?>\n\n    fun getVersionName(): String\n\n    suspend fun logout()\n\n    fun observeCacheSize(): Flow<Long>\n\n    suspend fun clearCache()\n}\n"
  },
  {
    "path": "feature/profile/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/profile/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.profile.domain)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n\n                implementation(libs.liquid)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt",
    "content": "package zed.rainxch.profile.presentation\n\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.profile.presentation.model.ProxyType\n\nsealed interface ProfileAction {\n    data object OnNavigateBackClick : ProfileAction\n\n    data class OnThemeColorSelected(\n        val themeColor: AppTheme,\n    ) : ProfileAction\n\n    data class OnAmoledThemeToggled(\n        val enabled: Boolean,\n    ) : ProfileAction\n\n    data class OnDarkThemeChange(\n        val isDarkTheme: Boolean?,\n    ) : ProfileAction\n\n    data object OnLogoutClick : ProfileAction\n\n    data object OnLogoutConfirmClick : ProfileAction\n\n    data object OnStarredReposClick : ProfileAction\n\n    data object OnFavouriteReposClick : ProfileAction\n\n    data object OnLogoutDismiss : ProfileAction\n\n    data object OnHelpClick : ProfileAction\n\n    data object OnLoginClick : ProfileAction\n\n    data object OnClearCacheClick : ProfileAction\n\n    data class OnFontThemeSelected(\n        val fontTheme: FontTheme,\n    ) : ProfileAction\n\n    data class OnProxyTypeSelected(\n        val type: ProxyType,\n    ) : ProfileAction\n\n    data class OnProxyHostChanged(\n        val host: String,\n    ) : ProfileAction\n\n    data class OnProxyPortChanged(\n        val port: String,\n    ) : ProfileAction\n\n    data class OnRepositoriesClick(\n        val username: String,\n    ) : ProfileAction\n\n    data class OnProxyUsernameChanged(\n        val username: String,\n    ) : ProfileAction\n\n    data class OnProxyPasswordChanged(\n        val password: String,\n    ) : ProfileAction\n\n    data object OnProxyPasswordVisibilityToggle : ProfileAction\n\n    data class OnLiquidGlassEnabledChange(\n        val enabled: Boolean,\n    ) : ProfileAction\n\n    data object OnProxySave : ProfileAction\n\n    data class OnInstallerTypeSelected(\n        val type: InstallerType,\n    ) : ProfileAction\n\n    data object OnRequestShizukuPermission : ProfileAction\n\n    data class OnAutoUpdateToggled(\n        val enabled: Boolean,\n    ) : ProfileAction\n\n    data class OnUpdateCheckIntervalChanged(\n        val hours: Long,\n    ) : ProfileAction\n\n    data class OnIncludePreReleasesToggled(\n        val enabled: Boolean,\n    ) : ProfileAction\n\n    data class OnAutoDetectClipboardToggled(\n        val enabled: Boolean,\n    ) : ProfileAction\n\n    data class OnHideSeenToggled(\n        val enabled: Boolean,\n    ) : ProfileAction\n\n    data object OnClearSeenRepos : ProfileAction\n\n    data object OnSponsorClick : ProfileAction\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt",
    "content": "package zed.rainxch.profile.presentation\n\nsealed interface ProfileEvent {\n    data object OnLogoutSuccessful : ProfileEvent\n\n    data class OnLogoutError(\n        val message: String,\n    ) : ProfileEvent\n\n    data object OnProxySaved : ProfileEvent\n\n    data class OnProxySaveError(\n        val message: String,\n    ) : ProfileEvent\n\n    data object OnCacheCleared : ProfileEvent\n\n    data class OnCacheClearError(\n        val message: String,\n    ) : ProfileEvent\n\n    data object OnSeenHistoryCleared : ProfileEvent\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt",
    "content": "package zed.rainxch.profile.presentation\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport io.github.fletchmckee.liquid.liquefiable\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.components.LogoutDialog\nimport zed.rainxch.profile.presentation.components.sections.about\nimport zed.rainxch.profile.presentation.components.sections.logout\nimport zed.rainxch.profile.presentation.components.sections.othersSection\nimport zed.rainxch.profile.presentation.components.sections.profile\nimport zed.rainxch.profile.presentation.components.sections.settings\n\n@Composable\nfun ProfileRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToDevProfile: (username: String) -> Unit,\n    onNavigateToAuthentication: () -> Unit,\n    onNavigateToStarredRepos: () -> Unit,\n    onNavigateToFavouriteRepos: () -> Unit,\n    onNavigateToSponsor: () -> Unit,\n    viewModel: ProfileViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    val snackbarState = remember { SnackbarHostState() }\n    val coroutineScope = rememberCoroutineScope()\n\n    ObserveAsEvents(viewModel.events) { event ->\n        when (event) {\n            ProfileEvent.OnLogoutSuccessful -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(getString(Res.string.logout_success))\n\n                    onNavigateBack()\n                }\n            }\n\n            is ProfileEvent.OnLogoutError -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(event.message)\n                }\n            }\n\n            ProfileEvent.OnProxySaved -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(getString(Res.string.proxy_saved))\n                }\n            }\n\n            is ProfileEvent.OnProxySaveError -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(event.message)\n                }\n            }\n\n            ProfileEvent.OnCacheCleared -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(getString(Res.string.cache_cleared))\n                }\n            }\n\n            is ProfileEvent.OnCacheClearError -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(event.message)\n                }\n            }\n\n            ProfileEvent.OnSeenHistoryCleared -> {\n                coroutineScope.launch {\n                    snackbarState.showSnackbar(getString(Res.string.seen_history_cleared))\n                }\n            }\n        }\n    }\n\n    ProfileScreen(\n        state = state,\n        onAction = { action ->\n            when (action) {\n                ProfileAction.OnNavigateBackClick -> {\n                    onNavigateBack()\n                }\n\n                ProfileAction.OnLoginClick -> {\n                    onNavigateToAuthentication()\n                }\n\n                ProfileAction.OnFavouriteReposClick -> {\n                    onNavigateToFavouriteRepos()\n                }\n\n                ProfileAction.OnStarredReposClick -> {\n                    onNavigateToStarredRepos()\n                }\n\n                is ProfileAction.OnRepositoriesClick -> {\n                    onNavigateToDevProfile(action.username)\n                }\n\n                ProfileAction.OnSponsorClick -> {\n                    onNavigateToSponsor()\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n        snackbarState = snackbarState,\n    )\n\n    if (state.isLogoutDialogVisible) {\n        LogoutDialog(\n            onDismissRequest = {\n                viewModel.onAction(ProfileAction.OnLogoutDismiss)\n            },\n            onLogout = {\n                viewModel.onAction(ProfileAction.OnLogoutConfirmClick)\n            },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ProfileScreen(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n    snackbarState: SnackbarHostState,\n) {\n    val liquidState = LocalBottomNavigationLiquid.current\n    val bottomNavHeight = LocalBottomNavigationHeight.current\n    Scaffold(\n        snackbarHost = {\n            SnackbarHost(\n                hostState = snackbarState,\n                modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp),\n            )\n        },\n        topBar = {\n            TopAppBar()\n        },\n        containerColor = MaterialTheme.colorScheme.background,\n        modifier =\n            Modifier.then(\n                if (state.isLiquidGlassEnabled) {\n                    Modifier.liquefiable(liquidState)\n                } else {\n                    Modifier\n                },\n            ),\n    ) { innerPadding ->\n        LazyColumn(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding)\n                    .padding(16.dp),\n        ) {\n            profile(\n                state = state,\n                onAction = onAction,\n            )\n\n            item {\n                Spacer(Modifier.height(32.dp))\n            }\n\n            settings(\n                state = state,\n                onAction = onAction,\n            )\n\n            item {\n                Spacer(Modifier.height(16.dp))\n            }\n\n            othersSection(\n                state = state,\n                onAction = onAction,\n            )\n\n            item {\n                Spacer(Modifier.height(32.dp))\n            }\n\n            about(\n                versionName = state.versionName,\n                onAction = onAction,\n            )\n\n            item {\n                Spacer(Modifier.height(32.dp))\n            }\n\n            if (state.isUserLoggedIn) {\n                logout(\n                    onAction = onAction,\n                )\n            }\n\n            item {\n                Spacer(Modifier.height(bottomNavHeight + 32.dp))\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopAppBar() {\n    TopAppBar(\n        title = {\n            Text(\n                text = stringResource(Res.string.profile_title),\n                style = MaterialTheme.typography.titleMediumEmphasized,\n                fontWeight = FontWeight.SemiBold,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n        },\n    )\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        ProfileScreen(\n            state = ProfileState(),\n            onAction = {},\n            snackbarState = SnackbarHostState(),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt",
    "content": "package zed.rainxch.profile.presentation\n\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.core.domain.model.ShizukuAvailability\nimport zed.rainxch.profile.domain.model.UserProfile\nimport zed.rainxch.profile.presentation.model.ProxyType\n\ndata class ProfileState(\n    val userProfile: UserProfile? = null,\n    val selectedThemeColor: AppTheme = AppTheme.OCEAN,\n    val selectedFontTheme: FontTheme = FontTheme.CUSTOM,\n    val isLogoutDialogVisible: Boolean = false,\n    val isUserLoggedIn: Boolean = false,\n    val isAmoledThemeEnabled: Boolean = false,\n    val isDarkTheme: Boolean? = null,\n    val versionName: String = \"\",\n    val proxyType: ProxyType = ProxyType.NONE,\n    val proxyHost: String = \"\",\n    val proxyPort: String = \"\",\n    val proxyUsername: String = \"\",\n    val proxyPassword: String = \"\",\n    val isProxyPasswordVisible: Boolean = false,\n    val autoDetectClipboardLinks: Boolean = true,\n    val cacheSize: String = \"\",\n    val installerType: InstallerType = InstallerType.DEFAULT,\n    val shizukuAvailability: ShizukuAvailability = ShizukuAvailability.UNAVAILABLE,\n    val autoUpdateEnabled: Boolean = false,\n    val updateCheckIntervalHours: Long = 6L,\n    val includePreReleases: Boolean = false,\n    val isLiquidGlassEnabled: Boolean = true,\n    val isHideSeenEnabled: Boolean = false,\n)\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt",
    "content": "package zed.rainxch.profile.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.core.domain.model.ProxyConfig\nimport zed.rainxch.core.domain.repository.ProxyRepository\nimport zed.rainxch.core.domain.repository.SeenReposRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.system.InstallerStatusProvider\nimport zed.rainxch.core.domain.system.UpdateScheduleManager\nimport zed.rainxch.core.domain.utils.BrowserHelper\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings\nimport zed.rainxch.githubstore.core.presentation.res.invalid_proxy_port\nimport zed.rainxch.githubstore.core.presentation.res.proxy_host_required\nimport zed.rainxch.profile.domain.repository.ProfileRepository\nimport zed.rainxch.profile.presentation.model.ProxyType\n\nclass ProfileViewModel(\n    private val browserHelper: BrowserHelper,\n    private val tweaksRepository: TweaksRepository,\n    private val profileRepository: ProfileRepository,\n    private val installerStatusProvider: InstallerStatusProvider,\n    private val proxyRepository: ProxyRepository,\n    private val updateScheduleManager: UpdateScheduleManager,\n    private val seenReposRepository: SeenReposRepository,\n) : ViewModel() {\n    private var userProfileJob: Job? = null\n\n    private var hasLoadedInitialData = false\n\n    private val _state = MutableStateFlow(ProfileState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    loadCurrentTheme()\n                    loadUserProfile()\n                    loadVersionName()\n                    loadProxyConfig()\n                    loadInstallerPreference()\n                    loadAutoUpdatePreference()\n                    loadUpdateCheckInterval()\n                    loadIncludePreReleases()\n                    loadLiquidGlassEnabled()\n                    loadHideSeenEnabled()\n\n                    observeLoggedInStatus()\n\n                    observeCacheSize()\n                    observeShizukuStatus()\n\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = ProfileState(),\n            )\n\n    private val _events = Channel<ProfileEvent>()\n    val events = _events.receiveAsFlow()\n\n    private fun observeCacheSize() {\n        viewModelScope.launch {\n            profileRepository.observeCacheSize().collect { sizeBytes ->\n                _state.update {\n                    it.copy(cacheSize = formatCacheSize(sizeBytes))\n                }\n            }\n        }\n    }\n\n    private fun formatCacheSize(bytes: Long): String {\n        if (bytes <= 0) return \"0 B\"\n        val units = arrayOf(\"B\", \"KB\", \"MB\", \"GB\")\n        var size = bytes.toDouble()\n        var unitIndex = 0\n        while (size >= 1024 && unitIndex < units.lastIndex) {\n            size /= 1024\n            unitIndex++\n        }\n        return if (size == size.toLong().toDouble()) {\n            \"${size.toLong()} ${units[unitIndex]}\"\n        } else {\n            \"${\"%.1f\".format(size)} ${units[unitIndex]}\"\n        }\n    }\n\n    private fun loadVersionName() {\n        viewModelScope.launch {\n            _state.update {\n                it.copy(\n                    versionName = profileRepository.getVersionName(),\n                )\n            }\n        }\n    }\n\n    private fun observeLoggedInStatus() {\n        viewModelScope.launch {\n            profileRepository.isUserLoggedIn\n                .collect { isLoggedIn ->\n                    _state.update { it.copy(isUserLoggedIn = isLoggedIn) }\n                    if (isLoggedIn) {\n                        loadUserProfile()\n                    } else {\n                        _state.update { it.copy(userProfile = null) }\n                    }\n                }\n        }\n    }\n\n    private fun loadUserProfile() {\n        userProfileJob?.cancel()\n\n        userProfileJob =\n            viewModelScope.launch {\n                profileRepository.getUser().collect { profile ->\n                    _state.update { it.copy(userProfile = profile) }\n                }\n            }\n    }\n\n    private fun loadCurrentTheme() {\n        viewModelScope.launch {\n            tweaksRepository.getThemeColor().collect { theme ->\n                _state.update {\n                    it.copy(selectedThemeColor = theme)\n                }\n            }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository.getAmoledTheme().collect { isAmoled ->\n                _state.update {\n                    it.copy(isAmoledThemeEnabled = isAmoled)\n                }\n            }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository.getIsDarkTheme().collect { isDarkTheme ->\n                _state.update {\n                    it.copy(isDarkTheme = isDarkTheme)\n                }\n            }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository.getFontTheme().collect { fontTheme ->\n                _state.update {\n                    it.copy(selectedFontTheme = fontTheme)\n                }\n            }\n        }\n\n        viewModelScope.launch {\n            tweaksRepository.getAutoDetectClipboardLinks().collect { enabled ->\n                _state.update {\n                    it.copy(autoDetectClipboardLinks = enabled)\n                }\n            }\n        }\n    }\n\n    private fun loadProxyConfig() {\n        viewModelScope.launch {\n            proxyRepository.getProxyConfig().collect { config ->\n                _state.update {\n                    it.copy(\n                        proxyType = ProxyType.fromConfig(config),\n                        proxyHost =\n                            when (config) {\n                                is ProxyConfig.Http -> config.host\n                                is ProxyConfig.Socks -> config.host\n                                else -> it.proxyHost\n                            },\n                        proxyPort =\n                            when (config) {\n                                is ProxyConfig.Http -> config.port.toString()\n                                is ProxyConfig.Socks -> config.port.toString()\n                                else -> it.proxyPort\n                            },\n                        proxyUsername =\n                            when (config) {\n                                is ProxyConfig.Http -> config.username ?: \"\"\n                                is ProxyConfig.Socks -> config.username ?: \"\"\n                                else -> it.proxyUsername\n                            },\n                        proxyPassword =\n                            when (config) {\n                                is ProxyConfig.Http -> config.password ?: \"\"\n                                is ProxyConfig.Socks -> config.password ?: \"\"\n                                else -> it.proxyPassword\n                            },\n                    )\n                }\n            }\n        }\n    }\n\n    private fun loadInstallerPreference() {\n        viewModelScope.launch {\n            tweaksRepository.getInstallerType().collect { type ->\n                _state.update {\n                    it.copy(installerType = type)\n                }\n            }\n        }\n    }\n\n    private fun observeShizukuStatus() {\n        viewModelScope.launch {\n            installerStatusProvider.shizukuAvailability.collect { availability ->\n                _state.update {\n                    it.copy(shizukuAvailability = availability)\n                }\n            }\n        }\n    }\n\n    private fun loadAutoUpdatePreference() {\n        viewModelScope.launch {\n            tweaksRepository.getAutoUpdateEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(autoUpdateEnabled = enabled)\n                }\n            }\n        }\n    }\n\n    private fun loadUpdateCheckInterval() {\n        viewModelScope.launch {\n            tweaksRepository.getUpdateCheckInterval().collect { hours ->\n                _state.update {\n                    it.copy(updateCheckIntervalHours = hours)\n                }\n            }\n        }\n    }\n\n    private fun loadLiquidGlassEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getLiquidGlassEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(isLiquidGlassEnabled = enabled)\n                }\n            }\n        }\n    }\n\n    private fun loadHideSeenEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getHideSeenEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(isHideSeenEnabled = enabled)\n                }\n            }\n        }\n    }\n\n    private fun loadIncludePreReleases() {\n        viewModelScope.launch {\n            tweaksRepository.getIncludePreReleases().collect { enabled ->\n                _state.update {\n                    it.copy(includePreReleases = enabled)\n                }\n            }\n        }\n    }\n\n    fun onAction(action: ProfileAction) {\n        when (action) {\n            ProfileAction.OnHelpClick -> {\n                browserHelper.openUrl(\n                    url = \"https://github.com/OpenHub-Store/GitHub-Store/issues\",\n                )\n            }\n\n            ProfileAction.OnClearCacheClick -> {\n                viewModelScope.launch {\n                    runCatching {\n                        profileRepository.clearCache()\n                    }.onSuccess {\n                        observeCacheSize()\n                        _events.send(ProfileEvent.OnCacheCleared)\n                    }.onFailure { error ->\n                        _events.send(\n                            ProfileEvent.OnCacheClearError(\n                                error.message ?: \"Failed to clear cache\",\n                            ),\n                        )\n                    }\n                }\n            }\n\n            is ProfileAction.OnThemeColorSelected -> {\n                viewModelScope.launch {\n                    tweaksRepository.setThemeColor(action.themeColor)\n                }\n            }\n\n            is ProfileAction.OnAmoledThemeToggled -> {\n                viewModelScope.launch {\n                    tweaksRepository.setAmoledTheme(action.enabled)\n                }\n            }\n\n            ProfileAction.OnLogoutClick -> {\n                _state.update {\n                    it.copy(\n                        isLogoutDialogVisible = true,\n                    )\n                }\n            }\n\n            ProfileAction.OnLogoutConfirmClick -> {\n                viewModelScope.launch {\n                    runCatching {\n                        profileRepository.logout()\n                    }.onSuccess {\n                        _state.update { it.copy(isLogoutDialogVisible = false, userProfile = null) }\n                        _events.send(ProfileEvent.OnLogoutSuccessful)\n                    }.onFailure { error ->\n                        _state.update { it.copy(isLogoutDialogVisible = false) }\n                        error.message?.let {\n                            _events.send(ProfileEvent.OnLogoutError(it))\n                        }\n                    }\n                }\n            }\n\n            ProfileAction.OnLogoutDismiss -> {\n                _state.update {\n                    it.copy(\n                        isLogoutDialogVisible = false,\n                    )\n                }\n            }\n\n            is ProfileAction.OnLiquidGlassEnabledChange -> {\n                viewModelScope.launch {\n                    tweaksRepository.setLiquidGlassEnabled(action.enabled)\n                }\n            }\n\n            ProfileAction.OnNavigateBackClick -> {\n                // Handed in composable\n            }\n\n            ProfileAction.OnLoginClick -> {\n                // Handed in composable\n            }\n\n            ProfileAction.OnFavouriteReposClick -> {\n                // Handed in composable\n            }\n\n            ProfileAction.OnStarredReposClick -> {\n                // Handed in composable\n            }\n\n            is ProfileAction.OnRepositoriesClick -> {\n                // Handed in composable\n            }\n\n            ProfileAction.OnSponsorClick -> {\n                // Handed in composable\n            }\n\n            is ProfileAction.OnFontThemeSelected -> {\n                viewModelScope.launch {\n                    tweaksRepository.setFontTheme(action.fontTheme)\n                }\n            }\n\n            is ProfileAction.OnDarkThemeChange -> {\n                viewModelScope.launch {\n                    tweaksRepository.setDarkTheme(action.isDarkTheme)\n                }\n            }\n\n            is ProfileAction.OnProxyTypeSelected -> {\n                _state.update { it.copy(proxyType = action.type) }\n                if (action.type == ProxyType.NONE || action.type == ProxyType.SYSTEM) {\n                    val config =\n                        when (action.type) {\n                            ProxyType.NONE -> ProxyConfig.None\n                            ProxyType.SYSTEM -> ProxyConfig.System\n                            else -> return\n                        }\n                    viewModelScope.launch {\n                        runCatching {\n                            proxyRepository.setProxyConfig(config)\n                        }.onSuccess {\n                            _events.send(ProfileEvent.OnProxySaved)\n                        }.onFailure { error ->\n                            _events.send(\n                                ProfileEvent.OnProxySaveError(\n                                    error.message ?: getString(Res.string.failed_to_save_proxy_settings),\n                                ),\n                            )\n                        }\n                    }\n                }\n            }\n\n            is ProfileAction.OnProxyHostChanged -> {\n                _state.update { it.copy(proxyHost = action.host) }\n            }\n\n            is ProfileAction.OnProxyPortChanged -> {\n                _state.update { it.copy(proxyPort = action.port) }\n            }\n\n            is ProfileAction.OnProxyUsernameChanged -> {\n                _state.update { it.copy(proxyUsername = action.username) }\n            }\n\n            is ProfileAction.OnProxyPasswordChanged -> {\n                _state.update { it.copy(proxyPassword = action.password) }\n            }\n\n            ProfileAction.OnProxyPasswordVisibilityToggle -> {\n                _state.update { it.copy(isProxyPasswordVisible = !it.isProxyPasswordVisible) }\n            }\n\n            is ProfileAction.OnAutoDetectClipboardToggled -> {\n                viewModelScope.launch {\n                    tweaksRepository.setAutoDetectClipboardLinks(action.enabled)\n                }\n            }\n\n            is ProfileAction.OnHideSeenToggled -> {\n                viewModelScope.launch {\n                    tweaksRepository.setHideSeenEnabled(action.enabled)\n                }\n            }\n\n            ProfileAction.OnClearSeenRepos -> {\n                viewModelScope.launch {\n                    seenReposRepository.clearAll()\n                    _events.send(ProfileEvent.OnSeenHistoryCleared)\n                }\n            }\n\n            is ProfileAction.OnInstallerTypeSelected -> {\n                viewModelScope.launch {\n                    tweaksRepository.setInstallerType(action.type)\n                }\n            }\n\n            ProfileAction.OnRequestShizukuPermission -> {\n                installerStatusProvider.requestShizukuPermission()\n            }\n\n            is ProfileAction.OnAutoUpdateToggled -> {\n                viewModelScope.launch {\n                    tweaksRepository.setAutoUpdateEnabled(action.enabled)\n                }\n            }\n\n            is ProfileAction.OnUpdateCheckIntervalChanged -> {\n                viewModelScope.launch {\n                    tweaksRepository.setUpdateCheckInterval(action.hours)\n                    updateScheduleManager.reschedule(action.hours)\n                }\n            }\n\n            is ProfileAction.OnIncludePreReleasesToggled -> {\n                viewModelScope.launch {\n                    tweaksRepository.setIncludePreReleases(action.enabled)\n                }\n            }\n\n            ProfileAction.OnProxySave -> {\n                val currentState = _state.value\n                val port =\n                    currentState.proxyPort\n                        .toIntOrNull()\n                        ?.takeIf { it in 1..65535 }\n                        ?: run {\n                            viewModelScope.launch {\n                                _events.send(ProfileEvent.OnProxySaveError(getString(Res.string.invalid_proxy_port)))\n                            }\n                            return\n                        }\n                val host =\n                    currentState.proxyHost.trim().takeIf { it.isNotBlank() } ?: run {\n                        viewModelScope.launch {\n                            _events.send(ProfileEvent.OnProxySaveError(getString(Res.string.proxy_host_required)))\n                        }\n                        return\n                    }\n\n                val username = currentState.proxyUsername.takeIf { it.isNotBlank() }\n                val password = currentState.proxyPassword.takeIf { it.isNotBlank() }\n\n                val config =\n                    when (currentState.proxyType) {\n                        ProxyType.HTTP -> ProxyConfig.Http(host, port, username, password)\n                        ProxyType.SOCKS -> ProxyConfig.Socks(host, port, username, password)\n                        ProxyType.NONE -> ProxyConfig.None\n                        ProxyType.SYSTEM -> ProxyConfig.System\n                    }\n\n                viewModelScope.launch {\n                    runCatching {\n                        proxyRepository.setProxyConfig(config)\n                    }.onSuccess {\n                        _events.send(ProfileEvent.OnProxySaved)\n                    }.onFailure { error ->\n                        _events.send(\n                            ProfileEvent.OnProxySaveError(\n                                error.message ?: getString(Res.string.failed_to_save_proxy_settings),\n                            ),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/SponsorScreen.kt",
    "content": "package zed.rainxch.profile.presentation\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.BugReport\nimport androidx.compose.material.icons.filled.Coffee\nimport androidx.compose.material.icons.filled.EmojiEvents\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.IosShare\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material.icons.filled.VolunteerActivism\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ElevatedButton\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SponsorScreen(onNavigateBack: () -> Unit) {\n    val uriHandler = LocalUriHandler.current\n    val onOpenUrl: (String) -> Unit = { url ->\n        runCatching { uriHandler.openUri(url) }\n    }\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = {\n                    Text(\n                        text = stringResource(Res.string.sponsor_title),\n                        style = MaterialTheme.typography.titleMediumEmphasized,\n                        fontWeight = FontWeight.SemiBold,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                },\n                navigationIcon = {\n                    IconButton(onClick = onNavigateBack) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = stringResource(Res.string.navigate_back),\n                        )\n                    }\n                },\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.background,\n    ) { innerPadding ->\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding)\n                    .verticalScroll(rememberScrollState())\n                    .padding(16.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            // Hero section\n            HeroSection()\n\n            // Golden Kodee voting CTA - the highlight\n            GoldenKodeeCard(\n                onRegisterClick = {\n                    onOpenUrl(\"https://golden-kodee.awardsplatform.com/\")\n                },\n                onVoteClick = {\n                    onOpenUrl(\"https://golden-kodee.awardsplatform.com/entry/vote/mNKjQxkX\")\n                },\n            )\n\n            // Financial support options\n            SponsorOptionCard(\n                icon = Icons.Filled.Favorite,\n                title = stringResource(Res.string.sponsor_github_sponsors),\n                description = stringResource(Res.string.sponsor_github_sponsors_desc),\n                onClick = {\n                    onOpenUrl(\"https://github.com/sponsors/rainxchzed\")\n                },\n            )\n\n            SponsorOptionCard(\n                icon = Icons.Filled.Coffee,\n                title = stringResource(Res.string.sponsor_buy_me_coffee),\n                description = stringResource(Res.string.sponsor_buy_me_coffee_desc),\n                onClick = {\n                    onOpenUrl(\"https://buymeacoffee.com/rainxchzed\")\n                },\n            )\n\n            Spacer(Modifier.height(8.dp))\n\n            // Other ways to help\n            OtherWaysSection(onOpenUrl = onOpenUrl)\n\n            // Thank you note\n            Text(\n                text = stringResource(Res.string.sponsor_thank_you),\n                style = MaterialTheme.typography.bodyLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                textAlign = TextAlign.Center,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp),\n            )\n\n            Spacer(Modifier.height(16.dp))\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun HeroSection() {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Icon(\n            imageVector = Icons.Filled.VolunteerActivism,\n            contentDescription = null,\n            modifier =\n                Modifier\n                    .size(64.dp)\n                    .clip(CircleShape)\n                    .background(\n                        Brush.linearGradient(\n                            listOf(\n                                MaterialTheme.colorScheme.primary,\n                                MaterialTheme.colorScheme.tertiary,\n                            ),\n                        ),\n                    ).padding(14.dp),\n            tint = MaterialTheme.colorScheme.onPrimary,\n        )\n\n        Spacer(Modifier.height(16.dp))\n\n        Text(\n            text = stringResource(Res.string.sponsor_hero_title),\n            style = MaterialTheme.typography.headlineSmallEmphasized,\n            fontWeight = FontWeight.Bold,\n            color = MaterialTheme.colorScheme.onBackground,\n            textAlign = TextAlign.Center,\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        Text(\n            text = stringResource(Res.string.sponsor_hero_subtitle),\n            style = MaterialTheme.typography.bodyLarge,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n            modifier = Modifier.padding(horizontal = 8.dp),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        Text(\n            text = stringResource(Res.string.sponsor_personal_note),\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n            modifier = Modifier.padding(horizontal = 8.dp),\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun GoldenKodeeCard(\n    onRegisterClick: () -> Unit,\n    onVoteClick: () -> Unit,\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.primaryContainer,\n            ),\n        shape = RoundedCornerShape(28.dp),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(20.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Icon(\n                imageVector = Icons.Filled.EmojiEvents,\n                contentDescription = null,\n                modifier = Modifier.size(48.dp),\n                tint = MaterialTheme.colorScheme.onPrimaryContainer,\n            )\n\n            Spacer(Modifier.height(12.dp))\n\n            Text(\n                text = stringResource(Res.string.sponsor_kodee_title),\n                style = MaterialTheme.typography.titleLargeEmphasized,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.onPrimaryContainer,\n                textAlign = TextAlign.Center,\n            )\n\n            Spacer(Modifier.height(8.dp))\n\n            Text(\n                text = stringResource(Res.string.sponsor_kodee_subtitle),\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),\n                textAlign = TextAlign.Center,\n            )\n\n            Spacer(Modifier.height(16.dp))\n\n            // Steps\n            Column(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clip(RoundedCornerShape(16.dp))\n                        .background(MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.08f))\n                        .padding(12.dp),\n                verticalArrangement = Arrangement.spacedBy(4.dp),\n            ) {\n                Text(\n                    text = stringResource(Res.string.sponsor_kodee_step1),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),\n                )\n                Text(\n                    text = stringResource(Res.string.sponsor_kodee_step2),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),\n                )\n                Text(\n                    text = stringResource(Res.string.sponsor_kodee_step3),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),\n                )\n            }\n\n            Spacer(Modifier.height(16.dp))\n\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                FilledTonalButton(\n                    onClick = onRegisterClick,\n                    modifier = Modifier.weight(1f),\n                    shape = RoundedCornerShape(16.dp),\n                    colors =\n                        ButtonDefaults.filledTonalButtonColors(\n                            containerColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.15f),\n                            contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                        ),\n                ) {\n                    Text(\n                        text = stringResource(Res.string.sponsor_kodee_register),\n                        style = MaterialTheme.typography.titleSmall,\n                        fontWeight = FontWeight.SemiBold,\n                    )\n                }\n\n                ElevatedButton(\n                    onClick = onVoteClick,\n                    modifier = Modifier.weight(1f),\n                    shape = RoundedCornerShape(16.dp),\n                    colors =\n                        ButtonDefaults.elevatedButtonColors(\n                            containerColor = MaterialTheme.colorScheme.primary,\n                            contentColor = MaterialTheme.colorScheme.onPrimary,\n                        ),\n                ) {\n                    Icon(\n                        imageVector = Icons.Filled.EmojiEvents,\n                        contentDescription = null,\n                        modifier = Modifier.size(18.dp),\n                    )\n                    Spacer(Modifier.width(6.dp))\n                    Text(\n                        text = stringResource(Res.string.sponsor_kodee_vote),\n                        style = MaterialTheme.typography.titleSmall,\n                        fontWeight = FontWeight.Bold,\n                    )\n                }\n            }\n\n            Spacer(Modifier.height(8.dp))\n\n            Text(\n                text = stringResource(Res.string.sponsor_kodee_deadline),\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f),\n                fontWeight = FontWeight.Medium,\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun SponsorOptionCard(\n    icon: ImageVector,\n    title: String,\n    description: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    ElevatedCard(\n        modifier = modifier.fillMaxWidth(),\n        onClick = onClick,\n        colors =\n            CardDefaults.elevatedCardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n            ),\n        shape = RoundedCornerShape(24.dp),\n    ) {\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier =\n                    Modifier\n                        .size(44.dp)\n                        .clip(CircleShape)\n                        .background(\n                            Brush.linearGradient(\n                                listOf(\n                                    MaterialTheme.colorScheme.primary,\n                                    MaterialTheme.colorScheme.secondary,\n                                ),\n                            ),\n                        ).padding(10.dp),\n                tint = MaterialTheme.colorScheme.onPrimary,\n            )\n\n            Column(modifier = Modifier.weight(1f)) {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.SemiBold,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n                Text(\n                    text = description,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun OtherWaysSection(onOpenUrl: (String) -> Unit) {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        Text(\n            text = stringResource(Res.string.sponsor_other_ways_title),\n            style = MaterialTheme.typography.titleSmall,\n            color = MaterialTheme.colorScheme.secondary,\n            fontWeight = FontWeight.Bold,\n            modifier = Modifier.padding(start = 8.dp),\n        )\n\n        Spacer(Modifier.height(4.dp))\n\n        ElevatedCard(\n            modifier = Modifier.fillMaxWidth(),\n            colors =\n                CardDefaults.elevatedCardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                ),\n            shape = RoundedCornerShape(24.dp),\n        ) {\n            Column(modifier = Modifier.padding(4.dp)) {\n                OtherWayItem(\n                    icon = Icons.Filled.Star,\n                    title = stringResource(Res.string.sponsor_star_repo),\n                    description = stringResource(Res.string.sponsor_star_repo_desc),\n                    onClick = {\n                        onOpenUrl(\"https://github.com/OpenHub-Store/GitHub-Store\")\n                    },\n                )\n\n                OtherWayItem(\n                    icon = Icons.Filled.BugReport,\n                    title = stringResource(Res.string.sponsor_report_bugs),\n                    description = stringResource(Res.string.sponsor_report_bugs_desc),\n                    onClick = {\n                        onOpenUrl(\"https://github.com/OpenHub-Store/GitHub-Store/issues\")\n                    },\n                )\n\n                OtherWayItem(\n                    icon = Icons.Filled.IosShare,\n                    title = stringResource(Res.string.sponsor_share),\n                    description = stringResource(Res.string.sponsor_share_desc),\n                    onClick = {\n                        onOpenUrl(\"https://github.com/OpenHub-Store/GitHub-Store\")\n                    },\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun OtherWayItem(\n    icon: ImageVector,\n    title: String,\n    description: String,\n    onClick: () -> Unit,\n) {\n    FilledTonalButton(\n        onClick = onClick,\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(20.dp),\n        colors =\n            ButtonDefaults.filledTonalButtonColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                contentColor = MaterialTheme.colorScheme.onSurface,\n            ),\n    ) {\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier = Modifier.size(24.dp),\n                tint = MaterialTheme.colorScheme.primary,\n            )\n\n            Column(modifier = Modifier.weight(1f)) {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleSmall,\n                    fontWeight = FontWeight.Medium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n                Text(\n                    text = description,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt",
    "content": "package zed.rainxch.profile.presentation.components\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.BasicAlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LogoutDialog(\n    onDismissRequest: () -> Unit,\n    onLogout: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    BasicAlertDialog(\n        onDismissRequest = onDismissRequest,\n        properties =\n            DialogProperties(\n                dismissOnClickOutside = false,\n                usePlatformDefaultWidth = false,\n            ),\n        modifier =\n            modifier\n                .padding(16.dp)\n                .clip(RoundedCornerShape(24.dp))\n                .background(MaterialTheme.colorScheme.surfaceContainerHigh)\n                .padding(16.dp),\n    ) {\n        Column(\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            Text(\n                text = stringResource(Res.string.warning),\n                style = MaterialTheme.typography.titleLarge,\n                color = MaterialTheme.colorScheme.onSurface,\n                fontWeight = FontWeight.Bold,\n            )\n\n            Text(\n                text = stringResource(Res.string.logout_confirmation),\n                style = MaterialTheme.typography.bodyLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n\n            Text(\n                text = stringResource(Res.string.logout_revocation_note),\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.outline,\n            )\n\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),\n            ) {\n                TextButton(\n                    onClick = {\n                        onDismissRequest()\n                    },\n                ) {\n                    Text(\n                        text = stringResource(Res.string.close),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                }\n\n                Button(\n                    onClick = onLogout,\n                    colors =\n                        ButtonDefaults.buttonColors(\n                            containerColor = MaterialTheme.colorScheme.errorContainer,\n                            contentColor = MaterialTheme.colorScheme.onErrorContainer,\n                        ),\n                ) {\n                    Text(\n                        text = stringResource(Res.string.logout),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/SectionText.kt",
    "content": "package zed.rainxch.profile.presentation.components\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SectionTitle(text: String) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.titleMediumEmphasized,\n        color = MaterialTheme.colorScheme.primary,\n        fontWeight = FontWeight.Bold,\n        modifier = Modifier.padding(start = 8.dp),\n    )\n}\n\n@Composable\nfun SectionHeader(text: String) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.titleSmall,\n        color = MaterialTheme.colorScheme.secondary,\n        fontWeight = FontWeight.Bold,\n        modifier = Modifier.padding(start = 8.dp),\n    )\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/About.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.Info\nimport androidx.compose.material.icons.filled.QuestionMark\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.components.SectionHeader\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.about(\n    versionName: String,\n    onAction: (ProfileAction) -> Unit,\n) {\n    item {\n        SectionHeader(\n            text = stringResource(Res.string.section_about),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        ElevatedCard(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(4.dp),\n            colors =\n                CardDefaults.elevatedCardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                ),\n            shape = RoundedCornerShape(32.dp),\n        ) {\n            AboutItem(\n                icon = Icons.Filled.Info,\n                title = stringResource(Res.string.version),\n                actions = {\n                    Text(\n                        text = versionName,\n                        style = MaterialTheme.typography.titleSmall,\n                        color = MaterialTheme.colorScheme.outline,\n                        modifier = Modifier.padding(horizontal = 8.dp),\n                    )\n                },\n            )\n\n            HorizontalDivider()\n\n            AboutItem(\n                icon = Icons.Filled.QuestionMark,\n                title = stringResource(Res.string.help_support),\n                actions = {\n                    IconButton(\n                        shape = IconButtonDefaults.shapes().shape,\n                        onClick = {\n                            onAction(ProfileAction.OnHelpClick)\n                        },\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                },\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun AboutItem(\n    icon: ImageVector,\n    title: String,\n    actions: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .padding(8.dp),\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        IconButton(\n            shapes = IconButtonDefaults.shapes(),\n            onClick = { },\n            colors =\n                IconButtonDefaults.iconButtonColors(\n                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n                ),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier = Modifier.size(24.dp),\n            )\n        }\n\n        Text(\n            text = title,\n            color = MaterialTheme.colorScheme.onSurface,\n            style = MaterialTheme.typography.titleMedium,\n            fontWeight = FontWeight.Medium,\n            modifier = Modifier.weight(1f),\n        )\n\n        actions.invoke()\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.automirrored.filled.Logout\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.logout(onAction: (ProfileAction) -> Unit) {\n    item {\n        Spacer(Modifier.height(8.dp))\n\n        ElevatedCard(\n            modifier = Modifier.fillMaxWidth(),\n            colors =\n                CardDefaults.elevatedCardColors(\n                    containerColor = MaterialTheme.colorScheme.errorContainer,\n                ),\n            shape = RoundedCornerShape(32.dp),\n            onClick = {\n                onAction(ProfileAction.OnLogoutClick)\n            },\n        ) {\n            AccountItem(\n                icon = Icons.AutoMirrored.Filled.Logout,\n                title = stringResource(Res.string.logout),\n                actions = {\n                    IconButton(\n                        onClick = {\n                            onAction(ProfileAction.OnLogoutClick)\n                        },\n                        colors =\n                            IconButtonDefaults.iconButtonColors(\n                                contentColor = MaterialTheme.colorScheme.onSurface,\n                            ),\n                    ) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                },\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun AccountItem(\n    icon: ImageVector,\n    title: String,\n    actions: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .padding(8.dp),\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        IconButton(\n            shapes = IconButtonDefaults.shapes(),\n            onClick = { },\n            colors =\n                IconButtonDefaults.iconButtonColors(\n                    containerColor = MaterialTheme.colorScheme.errorContainer,\n                    contentColor = MaterialTheme.colorScheme.onErrorContainer,\n                ),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier = Modifier.size(24.dp),\n            )\n        }\n\n        Text(\n            text = title,\n            color = MaterialTheme.colorScheme.onSurface,\n            style = MaterialTheme.typography.titleMedium,\n            fontWeight = FontWeight.Medium,\n            modifier = Modifier.weight(1f),\n        )\n\n        actions.invoke()\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AccountCircle\nimport androidx.compose.material.icons.outlined.AccountCircle\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.GitHubStoreImage\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.domain.model.UserProfile\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.accountSection(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    item {\n        Column(\n            modifier = Modifier.fillMaxWidth(),\n            verticalArrangement = Arrangement.spacedBy(4.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            if (state.userProfile == null) {\n                Icon(\n                    imageVector = Icons.Filled.AccountCircle,\n                    contentDescription = null,\n                    modifier =\n                        Modifier\n                            .size(100.dp)\n                            .clip(CircleShape)\n                            .background(MaterialTheme.colorScheme.surfaceContainerHigh)\n                            .padding(20.dp),\n                    tint = MaterialTheme.colorScheme.onSurface,\n                )\n            } else {\n                GitHubStoreImage(\n                    imageModel = {\n                        state.userProfile.imageUrl\n                    },\n                    modifier =\n                        Modifier\n                            .size(128.dp)\n                            .clip(CircleShape)\n                            .background(MaterialTheme.colorScheme.surfaceContainerHigh),\n                )\n\n                Spacer(Modifier.height(8.dp))\n            }\n\n            if (state.userProfile != null) {\n                val displayName =\n                    state.userProfile.name.takeIf { it.isNotBlank() }\n                        ?: state.userProfile.username\n                Text(\n                    text = displayName,\n                    style = MaterialTheme.typography.titleLargeEmphasized,\n                    color = MaterialTheme.colorScheme.onBackground,\n                    textAlign = TextAlign.Center,\n                )\n\n                Text(\n                    text = \"@${state.userProfile.username}\",\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    textAlign = TextAlign.Center,\n                )\n\n                state.userProfile.bio?.let { bio ->\n                    Text(\n                        text = bio,\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        textAlign = TextAlign.Center,\n                    )\n                }\n            } else {\n                Spacer(Modifier.height(8.dp))\n\n                Text(\n                    text = stringResource(Res.string.profile_sign_in_title),\n                    style = MaterialTheme.typography.titleLarge,\n                    color = MaterialTheme.colorScheme.onBackground,\n                    textAlign = TextAlign.Center,\n                )\n\n                Spacer(Modifier.height(4.dp))\n\n                Text(\n                    text = stringResource(Res.string.profile_sign_in_description),\n                    style = MaterialTheme.typography.bodyLarge,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    textAlign = TextAlign.Center,\n                )\n            }\n\n            if (state.userProfile != null) {\n                Spacer(Modifier.height(16.dp))\n\n                Row(\n                    modifier = Modifier.fillMaxSize(),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                ) {\n                    StatCard(\n                        label = stringResource(Res.string.profile_repos),\n                        value = state.userProfile.repositoryCount.toString(),\n                        modifier = Modifier.weight(1f),\n                        onClick = {\n                            onAction(ProfileAction.OnRepositoriesClick(state.userProfile.username))\n                        },\n                    )\n\n                    StatCard(\n                        label = stringResource(Res.string.followers),\n                        value = state.userProfile.followers.toString(),\n                        modifier = Modifier.weight(1f),\n                    )\n\n                    StatCard(\n                        label = stringResource(Res.string.following),\n                        value = state.userProfile.following.toString(),\n                        modifier = Modifier.weight(1f),\n                    )\n                }\n            }\n\n            if (state.userProfile == null) {\n                Spacer(Modifier.height(8.dp))\n\n                GithubStoreButton(\n                    text = stringResource(Res.string.profile_login),\n                    onClick = {\n                        onAction(ProfileAction.OnLoginClick)\n                    },\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 8.dp),\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun StatCard(\n    label: String,\n    value: String,\n    modifier: Modifier = Modifier,\n    onClick: (() -> Unit)? = null,\n) {\n    Card(\n        modifier = modifier,\n        colors =\n            CardDefaults.elevatedCardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                contentColor = MaterialTheme.colorScheme.onSurface,\n            ),\n        shape = RoundedCornerShape(32.dp),\n        border =\n            BorderStroke(\n                width = 1.dp,\n                color = MaterialTheme.colorScheme.secondary,\n            ),\n        onClick = { onClick?.invoke() },\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Text(\n                text = value,\n                maxLines = 1,\n                style = MaterialTheme.typography.titleLargeEmphasized,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n\n            Text(\n                text = label,\n                maxLines = 1,\n                style = MaterialTheme.typography.bodyLargeEmphasized,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun AccountSectionPreview() {\n    GithubStoreTheme {\n        LazyColumn {\n            accountSection(\n                state = ProfileState(),\n                onAction = { },\n            )\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun AccountSectionUserPreview() {\n    GithubStoreTheme {\n        LazyColumn {\n            accountSection(\n                state =\n                    ProfileState(\n                        userProfile =\n                            UserProfile(\n                                id = 1,\n                                imageUrl = \"\",\n                                name = \"Octocat\",\n                                username = \"the_octocat\",\n                                bio = \" Language Savant. If your repository's language is being reported incorrectly, send us a pull request! \",\n                                repositoryCount = 8,\n                                followers = 21900,\n                                following = 9,\n                            ),\n                    ),\n                onAction = { },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Colorize\nimport androidx.compose.material.icons.filled.DarkMode\nimport androidx.compose.material.icons.filled.Done\nimport androidx.compose.material.icons.filled.LightMode\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialShapes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.ripple\nimport androidx.compose.material3.toShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.model.AppTheme\nimport zed.rainxch.core.domain.model.FontTheme\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.core.presentation.theme.isDynamicColorAvailable\nimport zed.rainxch.core.presentation.utils.displayName\nimport zed.rainxch.core.presentation.utils.primaryColor\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\nimport zed.rainxch.profile.presentation.components.SectionHeader\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.appearanceSection(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    item {\n        SectionHeader(\n            text = stringResource(Res.string.section_appearance),\n        )\n\n        VerticalSpacer(8.dp)\n\n        ThemeSelectionCard(\n            isDarkTheme = state.isDarkTheme,\n            onDarkThemeChange = { isDarkTheme ->\n                onAction(ProfileAction.OnDarkThemeChange(isDarkTheme))\n            },\n        )\n\n        VerticalSpacer(12.dp)\n\n        ThemeColorCard(\n            selectedThemeColor = state.selectedThemeColor,\n            onThemeColorSelected = { theme ->\n                onAction(ProfileAction.OnThemeColorSelected(theme))\n            },\n        )\n\n        VerticalSpacer(16.dp)\n\n        if (state.isDarkTheme == true || (state.isDarkTheme == null && isSystemInDarkTheme())) {\n            ToggleSettingCard(\n                title = stringResource(Res.string.amoled_black_theme),\n                description = stringResource(Res.string.amoled_black_description),\n                checked = state.isAmoledThemeEnabled,\n                onCheckedChange = { enabled ->\n                    onAction(ProfileAction.OnAmoledThemeToggled(enabled))\n                },\n            )\n\n            VerticalSpacer(8.dp)\n        }\n\n        ToggleSettingCard(\n            title = stringResource(Res.string.system_font),\n            description = stringResource(Res.string.system_font_description),\n            checked = state.selectedFontTheme == FontTheme.SYSTEM,\n            onCheckedChange = { enabled ->\n                onAction(\n                    ProfileAction.OnFontThemeSelected(\n                        if (enabled) {\n                            FontTheme.SYSTEM\n                        } else {\n                            FontTheme.CUSTOM\n                        },\n                    ),\n                )\n            },\n        )\n\n        VerticalSpacer(8.dp)\n\n        ToggleSettingCard(\n            title = stringResource(Res.string.liquid_glass_option_title),\n            description = stringResource(Res.string.liquid_glass_option_description),\n            checked = state.isLiquidGlassEnabled,\n            onCheckedChange = { enabled ->\n                onAction(ProfileAction.OnLiquidGlassEnabledChange(enabled))\n            },\n        )\n\n        VerticalSpacer(8.dp)\n\n        ToggleSettingCard(\n            title = stringResource(Res.string.auto_detect_clipboard_links),\n            description = stringResource(Res.string.auto_detect_clipboard_description),\n            checked = state.autoDetectClipboardLinks,\n            onCheckedChange = { enabled ->\n                onAction(ProfileAction.OnAutoDetectClipboardToggled(enabled))\n            },\n        )\n\n        VerticalSpacer(8.dp)\n\n        ToggleSettingCard(\n            title = stringResource(Res.string.hide_seen_title),\n            description = stringResource(Res.string.hide_seen_description),\n            checked = state.isHideSeenEnabled,\n            onCheckedChange = { enabled ->\n                onAction(ProfileAction.OnHideSeenToggled(enabled))\n            },\n        )\n\n        VerticalSpacer(8.dp)\n\n        ClearSeenHistoryCard(\n            onClick = {\n                onAction(ProfileAction.OnClearSeenRepos)\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun VerticalSpacer(height: Dp) {\n    Spacer(Modifier.height(height))\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ThemeSelectionCard(\n    isDarkTheme: Boolean?,\n    onDarkThemeChange: (Boolean?) -> Unit,\n) {\n    ExpressiveCard {\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n            horizontalArrangement = Arrangement.spacedBy(12.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            ThemeModeOption(\n                icon = Icons.Default.LightMode,\n                label = stringResource(Res.string.theme_light),\n                isSelected = isDarkTheme != null && !isDarkTheme,\n                onClick = { onDarkThemeChange(false) },\n                modifier = Modifier.weight(1f),\n            )\n\n            ThemeModeOption(\n                icon = Icons.Default.DarkMode,\n                label = stringResource(Res.string.theme_dark),\n                isSelected = isDarkTheme == true,\n                onClick = { onDarkThemeChange(true) },\n                modifier = Modifier.weight(1f),\n            )\n\n            ThemeModeOption(\n                icon = Icons.Default.Colorize,\n                label = stringResource(Res.string.theme_system),\n                isSelected = isDarkTheme == null,\n                onClick = { onDarkThemeChange(null) },\n                modifier = Modifier.weight(1f),\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ThemeModeOption(\n    icon: ImageVector,\n    label: String,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    val scale by animateFloatAsState(\n        targetValue = if (isSelected) 1.05f else 1f,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessLow,\n            ),\n    )\n\n    Column(\n        modifier =\n            modifier\n                .scale(scale)\n                .clip(RoundedCornerShape(24.dp))\n                .background(\n                    if (isSelected) {\n                        MaterialTheme.colorScheme.primaryContainer\n                    } else {\n                        MaterialTheme.colorScheme.surface\n                    },\n                ).clickable(onClick = onClick)\n                .padding(vertical = 12.dp),\n        verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n            modifier = Modifier.size(24.dp),\n            tint =\n                if (isSelected) {\n                    MaterialTheme.colorScheme.onPrimaryContainer\n                } else {\n                    MaterialTheme.colorScheme.onSurfaceVariant\n                },\n        )\n\n        Text(\n            text = label,\n            style = MaterialTheme.typography.titleMedium,\n            color =\n                if (isSelected) {\n                    MaterialTheme.colorScheme.onPrimaryContainer\n                } else {\n                    MaterialTheme.colorScheme.onSurface\n                },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ThemeColorCard(\n    selectedThemeColor: AppTheme,\n    onThemeColorSelected: (AppTheme) -> Unit,\n) {\n    ExpressiveCard {\n        Column(\n            modifier = Modifier.padding(16.dp),\n        ) {\n            Text(\n                text = stringResource(Res.string.theme_color),\n                style = MaterialTheme.typography.titleMedium,\n                color = MaterialTheme.colorScheme.onSurface,\n                fontWeight = FontWeight.SemiBold,\n            )\n\n            VerticalSpacer(12.dp)\n\n            LazyRow(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(20.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                val availableThemes =\n                    if (isDynamicColorAvailable()) {\n                        AppTheme.entries\n                    } else {\n                        AppTheme.entries.filter { it != AppTheme.DYNAMIC }\n                    }\n\n                items(availableThemes) { theme ->\n                    ThemeColorOption(\n                        theme = theme,\n                        isSelected = selectedThemeColor == theme,\n                        onClick = { onThemeColorSelected(theme) },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ThemeColorOption(\n    theme: AppTheme,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n) {\n    val scale by animateFloatAsState(\n        targetValue = if (isSelected) 1.1f else 1f,\n        animationSpec =\n            spring(\n                dampingRatio = Spring.DampingRatioMediumBouncy,\n                stiffness = Spring.StiffnessLow,\n            ),\n    )\n\n    Column(\n        modifier = Modifier.clickable(onClick = onClick),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(56.dp)\n                    .scale(scale)\n                    .clip(\n                        if (isSelected) {\n                            MaterialShapes.Cookie9Sided.toShape()\n                        } else {\n                            CircleShape\n                        },\n                    ).background(\n                        color = theme.primaryColor ?: MaterialTheme.colorScheme.primary,\n                    ).then(\n                        if (theme == AppTheme.DYNAMIC) {\n                            Modifier.border(\n                                2.dp,\n                                MaterialTheme.colorScheme.outline,\n                                if (isSelected) {\n                                    MaterialShapes.Cookie9Sided.toShape()\n                                } else {\n                                    CircleShape\n                                },\n                            )\n                        } else {\n                            Modifier\n                        },\n                    ),\n            contentAlignment = Alignment.Center,\n        ) {\n            androidx.compose.animation.AnimatedVisibility(\n                visible = isSelected,\n                enter = scaleIn(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + fadeIn(),\n                exit = scaleOut() + fadeOut(),\n            ) {\n                Icon(\n                    imageVector = Icons.Default.Done,\n                    contentDescription =\n                        stringResource(\n                            Res.string.selected_color,\n                            theme.displayName,\n                        ),\n                    modifier = Modifier.size(28.dp),\n                    tint = MaterialTheme.colorScheme.onPrimary,\n                )\n            }\n        }\n\n        Text(\n            text = theme.displayName,\n            style = MaterialTheme.typography.labelLarge,\n            color =\n                if (isSelected) {\n                    MaterialTheme.colorScheme.primary\n                } else {\n                    MaterialTheme.colorScheme.onSurfaceVariant\n                },\n            fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n        )\n    }\n}\n\n@Composable\nprivate fun ToggleSettingCard(\n    title: String,\n    description: String,\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n) {\n    ExpressiveCard {\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .toggleable(\n                        value = checked,\n                        onValueChange = onCheckedChange,\n                        role = Role.Switch,\n                        interactionSource = remember { MutableInteractionSource() },\n                        indication = ripple(),\n                    ).padding(16.dp),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Column(\n                modifier =\n                    Modifier\n                        .weight(1f)\n                        .padding(end = 16.dp),\n            ) {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                    fontWeight = FontWeight.SemiBold,\n                )\n\n                VerticalSpacer(4.dp)\n\n                Text(\n                    text = description,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n\n            Switch(\n                checked = checked,\n                onCheckedChange = null,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ClearSeenHistoryCard(\n    onClick: () -> Unit,\n) {\n    ExpressiveCard {\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .clickable(onClick = onClick)\n                    .padding(16.dp),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Column(\n                modifier = Modifier.weight(1f),\n            ) {\n                Text(\n                    text = stringResource(Res.string.clear_seen_history),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                    fontWeight = FontWeight.SemiBold,\n                )\n\n                VerticalSpacer(4.dp)\n\n                Text(\n                    text = stringResource(Res.string.clear_seen_history_description),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Installation.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.selection.selectableGroup\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.InstallMobile\nimport androidx.compose.material.icons.outlined.Schedule\nimport androidx.compose.material.icons.outlined.Speed\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.domain.getPlatform\nimport zed.rainxch.core.domain.model.InstallerType\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.ShizukuAvailability\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\nimport zed.rainxch.profile.presentation.components.SectionHeader\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.installationSection(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    if (getPlatform() != Platform.ANDROID) return\n\n    item {\n        Spacer(Modifier.height(32.dp))\n\n        SectionHeader(\n            text = stringResource(Res.string.section_installation).uppercase()\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        InstallerTypeCard(\n            selectedType = state.installerType,\n            shizukuAvailability = state.shizukuAvailability,\n            onTypeSelected = { type ->\n                onAction(ProfileAction.OnInstallerTypeSelected(type))\n            },\n            onRequestPermission = {\n                onAction(ProfileAction.OnRequestShizukuPermission)\n            }\n        )\n\n        // Auto-update toggle — only shown when Shizuku is selected and ready\n        if (state.installerType == InstallerType.SHIZUKU &&\n            state.shizukuAvailability == ShizukuAvailability.READY\n        ) {\n            Spacer(Modifier.height(12.dp))\n\n            AutoUpdateCard(\n                enabled = state.autoUpdateEnabled,\n                onToggle = { enabled ->\n                    onAction(ProfileAction.OnAutoUpdateToggled(enabled))\n                }\n            )\n        }\n    }\n}\n\n/**\n * Updates section — always visible on Android (not gated on Shizuku).\n * Shows the update check interval picker so all users can configure\n * how often background update checks run.\n */\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.updatesSection(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    if (getPlatform() != Platform.ANDROID) return\n\n    item {\n        Spacer(Modifier.height(32.dp))\n\n        SectionHeader(\n            text = stringResource(Res.string.section_updates).uppercase()\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        UpdateCheckIntervalCard(\n            selectedIntervalHours = state.updateCheckIntervalHours,\n            onIntervalSelected = { hours ->\n                onAction(ProfileAction.OnUpdateCheckIntervalChanged(hours))\n            }\n        )\n\n        Spacer(Modifier.height(12.dp))\n\n        PreReleaseToggleCard(\n            enabled = state.includePreReleases,\n            onToggle = { enabled ->\n                onAction(ProfileAction.OnIncludePreReleasesToggled(enabled))\n            }\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun InstallerTypeCard(\n    selectedType: InstallerType,\n    shizukuAvailability: ShizukuAvailability,\n    onTypeSelected: (InstallerType) -> Unit,\n    onRequestPermission: () -> Unit\n) {\n    ExpressiveCard {\n        Column(\n            modifier = Modifier\n                .padding(16.dp)\n                .selectableGroup(),\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            InstallerOption(\n                icon = Icons.Outlined.InstallMobile,\n                title = stringResource(Res.string.installer_type_default),\n                description = stringResource(Res.string.installer_type_default_description),\n                isSelected = selectedType == InstallerType.DEFAULT,\n                onClick = { onTypeSelected(InstallerType.DEFAULT) }\n            )\n\n            InstallerOption(\n                icon = Icons.Outlined.Speed,\n                title = stringResource(Res.string.installer_type_shizuku),\n                description = stringResource(Res.string.installer_type_shizuku_description),\n                isSelected = selectedType == InstallerType.SHIZUKU,\n                onClick = { onTypeSelected(InstallerType.SHIZUKU) },\n                statusBadge = {\n                    ShizukuStatusBadge(\n                        availability = shizukuAvailability\n                    )\n                }\n            )\n\n            when (shizukuAvailability) {\n                ShizukuAvailability.PERMISSION_NEEDED -> {\n                    FilledTonalButton(\n                        onClick = onRequestPermission,\n                        modifier = Modifier.fillMaxWidth(),\n                        shape = RoundedCornerShape(12.dp)\n                    ) {\n                        Text(\n                            text = stringResource(Res.string.shizuku_grant_permission),\n                            style = MaterialTheme.typography.titleSmall,\n                            fontWeight = FontWeight.Bold\n                        )\n                    }\n                }\n\n                ShizukuAvailability.UNAVAILABLE -> {\n                    HintText(text = stringResource(Res.string.shizuku_install_hint))\n                }\n\n                ShizukuAvailability.NOT_RUNNING -> {\n                    HintText(text = stringResource(Res.string.shizuku_start_hint))\n                }\n\n                ShizukuAvailability.READY -> {\n                    // No hint needed\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun InstallerOption(\n    icon: ImageVector,\n    title: String,\n    description: String,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    statusBadge: (@Composable () -> Unit)? = null\n) {\n    val scale by animateFloatAsState(\n        targetValue = if (isSelected) 1.02f else 1f,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioMediumBouncy,\n            stiffness = Spring.StiffnessLow\n        )\n    )\n\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .scale(scale)\n            .clip(RoundedCornerShape(16.dp))\n            .background(\n                if (isSelected) {\n                    MaterialTheme.colorScheme.primaryContainer\n                } else {\n                    MaterialTheme.colorScheme.surface\n                }\n            )\n            .selectable(\n                selected = isSelected,\n                onClick = onClick,\n                role = Role.RadioButton\n            )\n            .padding(12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n            modifier = Modifier\n                .size(40.dp)\n                .clip(RoundedCornerShape(12.dp))\n                .background(\n                    if (isSelected) {\n                        MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)\n                    } else {\n                        MaterialTheme.colorScheme.surfaceContainerLow\n                    }\n                )\n                .padding(8.dp),\n            tint = if (isSelected) {\n                MaterialTheme.colorScheme.primary\n            } else {\n                MaterialTheme.colorScheme.onSurfaceVariant\n            }\n        )\n\n        Column(\n            modifier = Modifier.weight(1f),\n            verticalArrangement = Arrangement.spacedBy(2.dp)\n        ) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.titleMedium,\n                color = if (isSelected) {\n                    MaterialTheme.colorScheme.onPrimaryContainer\n                } else {\n                    MaterialTheme.colorScheme.onSurface\n                },\n                fontWeight = FontWeight.SemiBold\n            )\n\n            Text(\n                text = description,\n                style = MaterialTheme.typography.bodySmall,\n                color = if (isSelected) {\n                    MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)\n                } else {\n                    MaterialTheme.colorScheme.onSurfaceVariant\n                }\n            )\n        }\n\n        if (statusBadge != null) {\n            statusBadge()\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ShizukuStatusBadge(\n    availability: ShizukuAvailability\n) {\n    val (color, label) = when (availability) {\n        ShizukuAvailability.READY -> Pair(\n            Color(0xFF4CAF50),\n            stringResource(Res.string.shizuku_status_ready)\n        )\n\n        ShizukuAvailability.PERMISSION_NEEDED -> Pair(\n            Color(0xFFFF9800),\n            stringResource(Res.string.shizuku_status_permission_needed)\n        )\n\n        ShizukuAvailability.NOT_RUNNING -> Pair(\n            Color(0xFFFF5722),\n            stringResource(Res.string.shizuku_status_not_running)\n        )\n\n        ShizukuAvailability.UNAVAILABLE -> Pair(\n            MaterialTheme.colorScheme.outline,\n            stringResource(Res.string.shizuku_status_not_installed)\n        )\n    }\n\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(4.dp)\n    ) {\n        Box(\n            modifier = Modifier\n                .size(8.dp)\n                .clip(CircleShape)\n                .background(color)\n        )\n        Text(\n            text = label,\n            style = MaterialTheme.typography.labelSmall,\n            color = color,\n            fontWeight = FontWeight.Medium\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun AutoUpdateCard(\n    enabled: Boolean,\n    onToggle: (Boolean) -> Unit,\n) {\n    ExpressiveCard {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.spacedBy(2.dp)\n            ) {\n                Text(\n                    text = stringResource(Res.string.auto_update_title),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                    fontWeight = FontWeight.SemiBold\n                )\n                Text(\n                    text = stringResource(Res.string.auto_update_description),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n            Switch(\n                checked = enabled,\n                onCheckedChange = onToggle\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun UpdateCheckIntervalCard(\n    selectedIntervalHours: Long,\n    onIntervalSelected: (Long) -> Unit,\n) {\n    val intervals = listOf(\n        3L to Res.string.interval_3h,\n        6L to Res.string.interval_6h,\n        12L to Res.string.interval_12h,\n        24L to Res.string.interval_24h,\n    )\n\n    ExpressiveCard {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp),\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(12.dp)\n            ) {\n                Icon(\n                    imageVector = Icons.Outlined.Schedule,\n                    contentDescription = null,\n                    modifier = Modifier\n                        .size(40.dp)\n                        .clip(RoundedCornerShape(12.dp))\n                        .background(MaterialTheme.colorScheme.primaryContainer)\n                        .padding(8.dp),\n                    tint = MaterialTheme.colorScheme.onPrimaryContainer\n                )\n\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(2.dp)\n                ) {\n                    Text(\n                        text = stringResource(Res.string.update_check_interval_title),\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        fontWeight = FontWeight.SemiBold\n                    )\n                    Text(\n                        text = stringResource(Res.string.update_check_interval_description),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                }\n            }\n\n            FlowRow(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                intervals.forEach { (hours, labelRes) ->\n                    val isSelected = selectedIntervalHours == hours\n\n                    FilterChip(\n                        selected = isSelected,\n                        onClick = { onIntervalSelected(hours) },\n                        label = {\n                            Text(\n                                text = stringResource(labelRes),\n                                fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal\n                            )\n                        },\n                        shape = RoundedCornerShape(12.dp),\n                        colors = FilterChipDefaults.filterChipColors(\n                            selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,\n                            selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun PreReleaseToggleCard(\n    enabled: Boolean,\n    onToggle: (Boolean) -> Unit,\n) {\n    ExpressiveCard {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.spacedBy(2.dp)\n            ) {\n                Text(\n                    text = stringResource(Res.string.include_pre_releases_title),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                    fontWeight = FontWeight.SemiBold\n                )\n                Text(\n                    text = stringResource(Res.string.include_pre_releases_description),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n            Switch(\n                checked = enabled,\n                onCheckedChange = onToggle\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun HintText(text: String) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.bodySmall,\n        color = MaterialTheme.colorScheme.onSurfaceVariant,\n        modifier = Modifier.padding(start = 8.dp, top = 4.dp)\n    )\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material.icons.filled.Visibility\nimport androidx.compose.material.icons.filled.VisibilityOff\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.input.PasswordVisualTransformation\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\nimport zed.rainxch.profile.presentation.components.SectionHeader\nimport zed.rainxch.profile.presentation.model.ProxyType\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.networkSection(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    item {\n        SectionHeader(\n            text = stringResource(Res.string.section_network),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        ProxyTypeCard(\n            selectedType = state.proxyType,\n            onTypeSelected = { type ->\n                onAction(ProfileAction.OnProxyTypeSelected(type))\n            },\n        )\n\n        AnimatedVisibility(\n            visible = state.proxyType == ProxyType.NONE || state.proxyType == ProxyType.SYSTEM,\n            enter = expandVertically() + fadeIn(),\n            exit = shrinkVertically() + fadeOut(),\n        ) {\n            Text(\n                text =\n                    when (state.proxyType) {\n                        ProxyType.SYSTEM -> stringResource(Res.string.proxy_system_description)\n                        else -> stringResource(Res.string.proxy_none_description)\n                    },\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                modifier = Modifier.padding(start = 8.dp, top = 12.dp),\n            )\n        }\n\n        AnimatedVisibility(\n            visible = state.proxyType == ProxyType.HTTP || state.proxyType == ProxyType.SOCKS,\n            enter = expandVertically() + fadeIn(),\n            exit = shrinkVertically() + fadeOut(),\n        ) {\n            Column {\n                Spacer(Modifier.height(16.dp))\n\n                ProxyDetailsCard(\n                    state = state,\n                    onAction = onAction,\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ProxyTypeCard(\n    selectedType: ProxyType,\n    onTypeSelected: (ProxyType) -> Unit,\n) {\n    ElevatedCard(\n        modifier = Modifier.fillMaxWidth(),\n        colors =\n            CardDefaults.elevatedCardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainer,\n            ),\n        shape = RoundedCornerShape(32.dp),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n        ) {\n            Text(\n                text = stringResource(Res.string.proxy_type),\n                style = MaterialTheme.typography.titleMedium,\n                color = MaterialTheme.colorScheme.onSurface,\n                fontWeight = FontWeight.SemiBold,\n            )\n\n            Spacer(Modifier.height(8.dp))\n\n            LazyRow(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                items(ProxyType.entries) { type ->\n                    FilterChip(\n                        selected = selectedType == type,\n                        onClick = { onTypeSelected(type) },\n                        label = {\n                            Text(\n                                text =\n                                    when (type) {\n                                        ProxyType.NONE -> stringResource(Res.string.proxy_none)\n                                        ProxyType.SYSTEM -> stringResource(Res.string.proxy_system)\n                                        ProxyType.HTTP -> stringResource(Res.string.proxy_http)\n                                        ProxyType.SOCKS -> stringResource(Res.string.proxy_socks)\n                                    },\n                                fontWeight = if (selectedType == type) FontWeight.Bold else FontWeight.Normal,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurface,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ProxyDetailsCard(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    val portValue = state.proxyPort\n    val isPortInvalid =\n        portValue.isNotEmpty() &&\n            (portValue.toIntOrNull()?.let { it !in 1..65535 } ?: true)\n    val isFormValid =\n        state.proxyHost.isNotBlank() &&\n            portValue.isNotEmpty() &&\n            portValue.toIntOrNull()?.let { it in 1..65535 } == true\n\n    ElevatedCard(\n        modifier = Modifier.fillMaxWidth(),\n        colors =\n            CardDefaults.elevatedCardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainer,\n            ),\n        shape = RoundedCornerShape(32.dp),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                OutlinedTextField(\n                    value = state.proxyHost,\n                    onValueChange = { onAction(ProfileAction.OnProxyHostChanged(it)) },\n                    label = { Text(stringResource(Res.string.proxy_host)) },\n                    placeholder = { Text(\"127.0.0.1\") },\n                    singleLine = true,\n                    modifier = Modifier.weight(2f),\n                    shape = RoundedCornerShape(12.dp),\n                )\n\n                OutlinedTextField(\n                    value = state.proxyPort,\n                    onValueChange = { onAction(ProfileAction.OnProxyPortChanged(it)) },\n                    label = { Text(stringResource(Res.string.proxy_port)) },\n                    placeholder = { Text(\"1080\") },\n                    singleLine = true,\n                    isError = isPortInvalid,\n                    supportingText =\n                        if (isPortInvalid) {\n                            { Text(stringResource(Res.string.proxy_port_error)) }\n                        } else {\n                            null\n                        },\n                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),\n                    modifier = Modifier.weight(1f),\n                    shape = RoundedCornerShape(12.dp),\n                )\n            }\n\n            // Username\n            OutlinedTextField(\n                value = state.proxyUsername,\n                onValueChange = { onAction(ProfileAction.OnProxyUsernameChanged(it)) },\n                label = { Text(stringResource(Res.string.proxy_username)) },\n                singleLine = true,\n                modifier = Modifier.fillMaxWidth(),\n                shape = RoundedCornerShape(12.dp),\n            )\n\n            // Password with visibility toggle\n            OutlinedTextField(\n                value = state.proxyPassword,\n                onValueChange = { onAction(ProfileAction.OnProxyPasswordChanged(it)) },\n                label = { Text(stringResource(Res.string.proxy_password)) },\n                singleLine = true,\n                visualTransformation =\n                    if (state.isProxyPasswordVisible) {\n                        VisualTransformation.None\n                    } else {\n                        PasswordVisualTransformation()\n                    },\n                trailingIcon = {\n                    IconButton(\n                        onClick = { onAction(ProfileAction.OnProxyPasswordVisibilityToggle) },\n                    ) {\n                        Icon(\n                            imageVector =\n                                if (state.isProxyPasswordVisible) {\n                                    Icons.Default.VisibilityOff\n                                } else {\n                                    Icons.Default.Visibility\n                                },\n                            contentDescription =\n                                if (state.isProxyPasswordVisible) {\n                                    stringResource(Res.string.proxy_hide_password)\n                                } else {\n                                    stringResource(Res.string.proxy_show_password)\n                                },\n                            modifier = Modifier.size(20.dp),\n                        )\n                    }\n                },\n                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),\n                modifier = Modifier.fillMaxWidth(),\n                shape = RoundedCornerShape(12.dp),\n            )\n\n            // Save button\n            FilledTonalButton(\n                onClick = { onAction(ProfileAction.OnProxySave) },\n                enabled = isFormValid,\n                modifier = Modifier.align(Alignment.End),\n            ) {\n                Icon(\n                    imageVector = Icons.Default.Save,\n                    contentDescription = null,\n                    modifier = Modifier.size(18.dp),\n                )\n                Spacer(Modifier.size(8.dp))\n                Text(stringResource(Res.string.proxy_save))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material.icons.filled.VolunteerActivism\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\n\nfun LazyListScope.options(\n    isUserLoggedIn: Boolean,\n    onAction: (ProfileAction) -> Unit,\n) {\n    item {\n        OptionCard(\n            icon = Icons.Default.Star,\n            label = stringResource(Res.string.stars),\n            description = stringResource(Res.string.profile_stars_description),\n            onClick = {\n                onAction(ProfileAction.OnStarredReposClick)\n            },\n            enabled = isUserLoggedIn,\n        )\n\n        Spacer(Modifier.height(4.dp))\n\n        OptionCard(\n            icon = Icons.Default.Favorite,\n            label = stringResource(Res.string.favourites),\n            description = stringResource(Res.string.profile_favourites_description),\n            onClick = {\n                onAction(ProfileAction.OnFavouriteReposClick)\n            },\n        )\n\n        Spacer(Modifier.height(4.dp))\n\n        SponsorCard(\n            onClick = {\n                onAction(ProfileAction.OnSponsorClick)\n            },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun OptionCard(\n    icon: ImageVector,\n    label: String,\n    description: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n) {\n    Card(\n        modifier = modifier,\n        colors =\n            CardDefaults.elevatedCardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                contentColor = MaterialTheme.colorScheme.onSurface,\n                disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = .7f),\n                disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .7f),\n            ),\n        onClick = onClick,\n        shape = RoundedCornerShape(32.dp),\n        border =\n            BorderStroke(\n                width = .5.dp,\n                color = MaterialTheme.colorScheme.surface,\n            ),\n        enabled = enabled,\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier =\n                    Modifier\n                        .size(36.dp)\n                        .clip(CircleShape)\n                        .background(\n                            Brush.linearGradient(\n                                listOf(\n                                    MaterialTheme.colorScheme.primary,\n                                    MaterialTheme.colorScheme.secondary,\n                                ),\n                            ),\n                        ).padding(6.dp),\n                tint = MaterialTheme.colorScheme.onPrimary,\n            )\n\n            Column(\n                modifier =\n                    Modifier\n                        .weight(1f)\n                        .padding(12.dp),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.Start,\n            ) {\n                Text(\n                    text = label,\n                    maxLines = 1,\n                    style = MaterialTheme.typography.titleMedium,\n                    overflow = TextOverflow.Ellipsis,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n\n                Text(\n                    text = description,\n                    maxLines = 2,\n                    style = MaterialTheme.typography.bodyLargeEmphasized,\n                    overflow = TextOverflow.Ellipsis,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun SponsorCard(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Card(\n        modifier = modifier,\n        onClick = onClick,\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n            ),\n        shape = RoundedCornerShape(32.dp),\n        border =\n            BorderStroke(\n                width = 1.dp,\n                color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n            ),\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            Icon(\n                imageVector = Icons.Default.VolunteerActivism,\n                contentDescription = null,\n                modifier =\n                    Modifier\n                        .size(36.dp)\n                        .clip(CircleShape)\n                        .background(\n                            Brush.linearGradient(\n                                listOf(\n                                    MaterialTheme.colorScheme.primary,\n                                    MaterialTheme.colorScheme.tertiary,\n                                ),\n                            ),\n                        ).padding(6.dp),\n                tint = MaterialTheme.colorScheme.onPrimary,\n            )\n\n            Column(\n                modifier =\n                    Modifier\n                        .weight(1f)\n                        .padding(12.dp),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.Start,\n            ) {\n                Text(\n                    text = stringResource(Res.string.sponsor_button),\n                    maxLines = 1,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.SemiBold,\n                    overflow = TextOverflow.Ellipsis,\n                    color = MaterialTheme.colorScheme.onPrimaryContainer,\n                )\n\n                Text(\n                    text = stringResource(Res.string.sponsor_hero_subtitle),\n                    maxLines = 2,\n                    style = MaterialTheme.typography.bodySmall,\n                    overflow = TextOverflow.Ellipsis,\n                    color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f),\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Others.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Storage\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\nimport zed.rainxch.profile.presentation.components.SectionHeader\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nfun LazyListScope.othersSection(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    item {\n        SectionHeader(\n            text = stringResource(Res.string.storage).uppercase(),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        ExpressiveCard {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                Icon(\n                    imageVector = Icons.Outlined.Storage,\n                    contentDescription = null,\n                    modifier =\n                        Modifier\n                            .size(44.dp)\n                            .clip(RoundedCornerShape(36.dp))\n                            .background(MaterialTheme.colorScheme.surfaceContainerLow)\n                            .padding(8.dp),\n                )\n\n                Column(\n                    modifier = Modifier.weight(1f),\n                    verticalArrangement = Arrangement.spacedBy(2.dp),\n                    horizontalAlignment = Alignment.Start,\n                ) {\n                    Text(\n                        text = stringResource(Res.string.clear_cache),\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n\n                    Text(\n                        text = \"${stringResource(Res.string.current_size)} ${state.cacheSize}\",\n                        style = MaterialTheme.typography.titleSmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n\n                Button(\n                    onClick = {\n                        onAction(ProfileAction.OnClearCacheClick)\n                    },\n                    shape = RoundedCornerShape(12.dp),\n                    colors =\n                        ButtonDefaults.buttonColors(\n                            containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                            contentColor = MaterialTheme.colorScheme.onSurface,\n                        ),\n                ) {\n                    Text(\n                        text = stringResource(Res.string.clear),\n                        style = MaterialTheme.typography.titleMediumEmphasized,\n                        fontWeight = FontWeight.Bold,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/ProfileSection.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\n\nfun LazyListScope.profile(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    accountSection(\n        state = state,\n        onAction = onAction,\n    )\n\n    item {\n        Spacer(Modifier.height(20.dp))\n    }\n\n    options(\n        isUserLoggedIn = state.isUserLoggedIn,\n        onAction = onAction,\n    )\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/SettingsSection.kt",
    "content": "package zed.rainxch.profile.presentation.components.sections\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport zed.rainxch.profile.presentation.ProfileAction\nimport zed.rainxch.profile.presentation.ProfileState\n\nfun LazyListScope.settings(\n    state: ProfileState,\n    onAction: (ProfileAction) -> Unit,\n) {\n    appearanceSection(\n        state = state,\n        onAction = onAction,\n    )\n\n    item {\n        Spacer(Modifier.height(32.dp))\n    }\n\n    networkSection(\n        state = state,\n        onAction = onAction,\n    )\n\n    item {\n        Spacer(Modifier.height(12.dp))\n    }\n\n    installationSection(\n        state = state,\n        onAction = onAction,\n    )\n\n    updatesSection(\n        state = state,\n        onAction = onAction,\n    )\n}\n"
  },
  {
    "path": "feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt",
    "content": "package zed.rainxch.profile.presentation.model\n\nimport zed.rainxch.core.domain.model.ProxyConfig\n\nenum class ProxyType {\n    NONE,\n    SYSTEM,\n    HTTP,\n    SOCKS,\n    ;\n\n    companion object {\n        fun fromConfig(config: ProxyConfig): ProxyType =\n            when (config) {\n                is ProxyConfig.None -> NONE\n                is ProxyConfig.System -> SYSTEM\n                is ProxyConfig.Http -> HTTP\n                is ProxyConfig.Socks -> SOCKS\n            }\n    }\n}\n"
  },
  {
    "path": "feature/search/CLAUDE.md",
    "content": "# CLAUDE.md - Search Feature\n\n## Purpose\n\nRepository search with advanced filters. Users can search GitHub repositories by query and filter by platform (Android, Windows, macOS, Linux), programming language, and sort order. Supports paginated results.\n\n## Module Structure\n\n```\nfeature/search/\n├── domain/\n│   ├── model/\n│   │   ├── SearchPlatform.kt         # All, Android, Windows, macOS, Linux\n│   │   ├── ProgrammingLanguage.kt    # Language filter options\n│   │   └── SortBy.kt                 # Sort options (stars, updated, etc.)\n│   └── repository/SearchRepository.kt  # Filtered, paginated search\n├── data/\n│   ├── di/SharedModule.kt            # Koin: searchModule\n│   ├── repository/SearchRepositoryImpl.kt  # GitHub search API integration\n│   ├── dto/                           # Network DTOs\n│   └── mappers/                       # DTO → domain model mappers\n└── presentation/\n    ├── SearchViewModel.kt             # Search state, filter management, pagination\n    ├── SearchState.kt                 # query, results, filters, loading state\n    ├── SearchAction.kt                # Search, filter changes, load more, clicks\n    ├── SearchEvent.kt                 # One-off events\n    ├── SearchRoot.kt                  # Main composable with search bar + filter dropdowns\n    └── components/                    # Filter UI components\n```\n\n## Key Interfaces\n\n```kotlin\ninterface SearchRepository {\n    fun searchRepositories(\n        query: String,\n        searchPlatform: SearchPlatform,\n        language: ProgrammingLanguage,\n        sortBy: SortBy,\n        page: Int\n    ): Flow<PaginatedDiscoveryRepositories>\n}\n```\n\n## Navigation\n\nRoute: `GithubStoreGraph.SearchScreen` (data object, no params)\n\n## Implementation Notes\n\n- Platform filter maps to GitHub topic searches (e.g., `android` topic for Android platform)\n- Language filter maps to GitHub's `language:` qualifier\n- Search results use the same `PaginatedDiscoveryRepositories` model as home feature\n- Debounce/throttle applied to search queries to avoid excessive API calls\n- Integrates with favourites and starred status from core repositories\n"
  },
  {
    "path": "feature/search/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/search/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.data)\n                implementation(projects.feature.search.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n                implementation(libs.bundles.ktor.common)\n                implementation(libs.bundles.koin.common)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/search/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt",
    "content": "package zed.rainxch.search.data.di\n\nimport org.koin.dsl.module\nimport zed.rainxch.domain.repository.SearchRepository\nimport zed.rainxch.search.data.repository.SearchRepositoryImpl\n\nval searchModule =\n    module {\n        single<SearchRepository> {\n            SearchRepositoryImpl(\n                httpClient = get(),\n                cacheManager = get(),\n            )\n        }\n    }\n"
  },
  {
    "path": "feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/dto/AssetNetworkModel.kt",
    "content": "package zed.rainxch.search.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AssetNetworkModel(\n    @SerialName(\"name\") val name: String,\n)\n"
  },
  {
    "path": "feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/dto/GithubReleaseNetworkModel.kt",
    "content": "package zed.rainxch.search.data.dto\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GithubReleaseNetworkModel(\n    @SerialName(\"draft\") val draft: Boolean? = null,\n    @SerialName(\"prerelease\") val prerelease: Boolean? = null,\n    @SerialName(\"assets\") val assets: List<AssetNetworkModel>,\n    @SerialName(\"published_at\") val publishedAt: String? = null,\n)\n"
  },
  {
    "path": "feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt",
    "content": "package zed.rainxch.search.data.repository\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.channelFlow\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.sync.withPermit\nimport kotlinx.coroutines.withTimeoutOrNull\nimport zed.rainxch.core.data.cache.CacheManager\nimport zed.rainxch.core.data.cache.CacheManager.CacheTtl.SEARCH_RESULTS\nimport zed.rainxch.core.data.dto.GithubRepoNetworkModel\nimport zed.rainxch.core.data.dto.GithubRepoSearchResponse\nimport zed.rainxch.core.data.mappers.toSummary\nimport zed.rainxch.core.data.network.executeRequest\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.GithubRepoSummary\nimport zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.domain.model.ProgrammingLanguage\nimport zed.rainxch.domain.model.SortBy\nimport zed.rainxch.domain.model.SortOrder\nimport zed.rainxch.domain.repository.SearchRepository\nimport zed.rainxch.search.data.dto.GithubReleaseNetworkModel\nimport zed.rainxch.search.data.utils.LruCache\n\nclass SearchRepositoryImpl(\n    private val httpClient: HttpClient,\n    private val cacheManager: CacheManager,\n) : SearchRepository {\n    private val releaseCheckCache = LruCache<String, GithubRepoSummary>(maxSize = 500)\n    private val cacheMutex = Mutex()\n\n    companion object {\n        private const val PER_PAGE = 100\n        private const val VERIFY_CONCURRENCY = 15\n        private const val PER_CHECK_TIMEOUT_MS = 2000L\n        private const val MAX_AUTO_SKIP_PAGES = 3\n    }\n\n    private fun searchCacheKey(\n        query: String,\n        platform: DiscoveryPlatform,\n        language: ProgrammingLanguage,\n        sortBy: SortBy,\n        sortOrder: SortOrder,\n        page: Int,\n    ): String {\n        val queryHash =\n            query\n                .trim()\n                .lowercase()\n                .hashCode()\n                .toUInt()\n                .toString(16)\n        return \"search:$queryHash:${platform.name}:${language.name}:${sortBy.name}:${sortOrder.name}:page$page\"\n    }\n\n    override fun searchRepositories(\n        query: String,\n        platform: DiscoveryPlatform,\n        language: ProgrammingLanguage,\n        sortBy: SortBy,\n        sortOrder: SortOrder,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories> =\n        channelFlow {\n            val cacheKey = searchCacheKey(query, platform, language, sortBy, sortOrder, page)\n\n            val cached = cacheManager.get<PaginatedDiscoveryRepositories>(cacheKey)\n            if (cached != null) {\n                send(cached)\n                return@channelFlow\n            }\n\n            val searchQuery = buildSearchQuery(query, language)\n            val sort = sortBy.toGithubSortParam()\n            val order = sortOrder.toGithubParam()\n\n            try {\n                var currentPage = page\n                var pagesSkipped = 0\n\n                while (pagesSkipped <= MAX_AUTO_SKIP_PAGES) {\n                    currentCoroutineContext().ensureActive()\n\n                    val response =\n                        httpClient\n                            .executeRequest<GithubRepoSearchResponse> {\n                                get(\"/search/repositories\") {\n                                    parameter(\"q\", searchQuery)\n                                    parameter(\"per_page\", PER_PAGE)\n                                    parameter(\"page\", currentPage)\n                                    if (sort != null) {\n                                        parameter(\"sort\", sort)\n                                        parameter(\"order\", order)\n                                    }\n                                }\n                            }.getOrThrow()\n\n                    val total = response.totalCount\n                    val baseHasMore =\n                        (currentPage * PER_PAGE) < total && response.items.isNotEmpty()\n\n                    if (response.items.isEmpty()) {\n                        send(\n                            PaginatedDiscoveryRepositories(\n                                repos = emptyList(),\n                                hasMore = false,\n                                nextPageIndex = currentPage + 1,\n                                totalCount = total,\n                            ),\n                        )\n                        return@channelFlow\n                    }\n\n                    val verified = verifyBatch(response.items, platform)\n\n                    if (verified.isNotEmpty()) {\n                        val result =\n                            PaginatedDiscoveryRepositories(\n                                repos = verified,\n                                hasMore = baseHasMore,\n                                nextPageIndex = currentPage + 1,\n                                totalCount = total,\n                            )\n                        cacheManager.put(cacheKey, result, SEARCH_RESULTS)\n                        send(result)\n                        return@channelFlow\n                    }\n\n                    if (!baseHasMore) {\n                        send(\n                            PaginatedDiscoveryRepositories(\n                                repos = emptyList(),\n                                hasMore = false,\n                                nextPageIndex = currentPage + 1,\n                                totalCount = total,\n                            ),\n                        )\n                        return@channelFlow\n                    }\n\n                    currentPage++\n                    pagesSkipped++\n                }\n\n                send(\n                    PaginatedDiscoveryRepositories(\n                        repos = emptyList(),\n                        hasMore = true,\n                        nextPageIndex = currentPage + 1,\n                        totalCount = null,\n                    ),\n                )\n            } catch (e: RateLimitException) {\n                throw e\n            } catch (e: CancellationException) {\n                throw e\n            }\n        }.flowOn(Dispatchers.IO)\n\n    private suspend fun verifyBatch(\n        items: List<GithubRepoNetworkModel>,\n        searchPlatform: DiscoveryPlatform,\n    ): List<GithubRepoSummary> {\n        val semaphore = Semaphore(VERIFY_CONCURRENCY)\n\n        val deferredChecks =\n            coroutineScope {\n                items.map { repo ->\n                    async {\n                        try {\n                            semaphore.withPermit {\n                                withTimeoutOrNull(PER_CHECK_TIMEOUT_MS) {\n                                    checkRepoHasInstallersCached(repo, searchPlatform)\n                                }\n                            }\n                        } catch (_: CancellationException) {\n                            null\n                        }\n                    }\n                }\n            }\n\n        return buildList {\n            for (i in items.indices) {\n                currentCoroutineContext().ensureActive()\n                val result =\n                    try {\n                        deferredChecks[i].await()\n                    } catch (e: CancellationException) {\n                        throw e\n                    } catch (_: Exception) {\n                        null\n                    }\n                if (result != null) add(result)\n            }\n        }\n    }\n\n    private fun buildSearchQuery(\n        userQuery: String,\n        language: ProgrammingLanguage,\n    ): String {\n        val clean = userQuery.trim()\n        val q =\n            if (clean.isBlank()) {\n                \"stars:>100\"\n            } else {\n                \"\\\"$clean\\\"\"\n            }\n        val scope = \" in:name,description\"\n        val common = \" archived:false fork:true\"\n\n        val languageFilter =\n            if (language != ProgrammingLanguage.All && language.queryValue != null) {\n                \" language:${language.queryValue}\"\n            } else {\n                \"\"\n            }\n\n        return (\"$q$scope$common\" + languageFilter).trim()\n    }\n\n    private fun assetMatchesPlatform(\n        nameRaw: String,\n        platform: DiscoveryPlatform,\n    ): Boolean {\n        val name = nameRaw.lowercase()\n        return when (platform) {\n            DiscoveryPlatform.All -> {\n                name.endsWith(\".apk\") ||\n                    name.endsWith(\".msi\") || name.endsWith(\".exe\") ||\n                    name.endsWith(\".dmg\") || name.endsWith(\".pkg\") ||\n                    name.endsWith(\".appimage\") || name.endsWith(\".deb\") || name.endsWith(\".rpm\")\n            }\n\n            DiscoveryPlatform.Android -> {\n                name.endsWith(\".apk\")\n            }\n\n            DiscoveryPlatform.Windows -> {\n                name.endsWith(\".exe\") || name.endsWith(\".msi\")\n            }\n\n            DiscoveryPlatform.Macos -> {\n                name.endsWith(\".dmg\") || name.endsWith(\".pkg\")\n            }\n\n            DiscoveryPlatform.Linux -> {\n                name.endsWith(\".appimage\") || name.endsWith(\".deb\") || name.endsWith(\".rpm\")\n            }\n        }\n    }\n\n    private fun detectAvailablePlatforms(assetNames: List<String>): List<DiscoveryPlatform> =\n        buildList {\n            DiscoveryPlatform.entries\n                .filter { it != DiscoveryPlatform.All }\n                .forEach { platform ->\n                    if (assetNames.any { assetMatchesPlatform(it, platform) }) {\n                        add(platform)\n                    }\n                }\n        }\n\n    private suspend fun checkRepoHasInstallers(\n        repo: GithubRepoNetworkModel,\n        targetPlatform: DiscoveryPlatform,\n    ): GithubRepoSummary? {\n        return try {\n            val allReleases =\n                httpClient\n                    .executeRequest<List<GithubReleaseNetworkModel>> {\n                        get(\"/repos/${repo.owner.login}/${repo.name}/releases\") {\n                            header(\"Accept\", \"application/vnd.github.v3+json\")\n                            parameter(\"per_page\", 5)\n                        }\n                    }.getOrNull() ?: return null\n\n            val stableRelease =\n                allReleases.firstOrNull {\n                    it.draft != true && it.prerelease != true\n                }\n\n            if (stableRelease == null || stableRelease.assets.isEmpty()) {\n                return null\n            }\n\n            val hasRelevantAssets =\n                stableRelease.assets.any { asset ->\n                    assetMatchesPlatform(asset.name, targetPlatform)\n                }\n\n            if (hasRelevantAssets) {\n                val assetNames = stableRelease.assets.map { it.name }\n                val platforms = detectAvailablePlatforms(assetNames)\n                val summary = repo.toSummary()\n                summary.copy(\n                    updatedAt = stableRelease.publishedAt ?: summary.updatedAt,\n                    availablePlatforms = platforms,\n                )\n            } else {\n                null\n            }\n        } catch (_: Exception) {\n            null\n        }\n    }\n\n    private suspend fun checkRepoHasInstallersCached(\n        repo: GithubRepoNetworkModel,\n        targetPlatform: DiscoveryPlatform,\n    ): GithubRepoSummary? {\n        val key = \"${repo.owner.login}/${repo.name}:LATEST_PLATFORM_${targetPlatform.name}\"\n        val cached =\n            cacheMutex.withLock {\n                if (releaseCheckCache.contains(key)) releaseCheckCache.get(key) else null\n            }\n        if (cached != null ||\n            cacheMutex.withLock {\n                releaseCheckCache.contains(key) && releaseCheckCache.get(key) == null\n            }\n        ) {\n            return cached\n        }\n\n        val result = checkRepoHasInstallers(repo, targetPlatform)\n        cacheMutex.withLock {\n            releaseCheckCache.put(key, result)\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/utils/LruCache.kt",
    "content": "package zed.rainxch.search.data.utils\n\nclass LruCache<K, V>(\n    private val maxSize: Int,\n) {\n    private val map = LinkedHashMap<K, V?>()\n    private val order = ArrayDeque<K>()\n\n    fun get(key: K): V? {\n        val value = map[key]\n        if (value != null || map.containsKey(key)) {\n            order.remove(key)\n            order.addLast(key)\n        }\n        return value\n    }\n\n    fun put(\n        key: K,\n        value: V?,\n    ) {\n        map[key] = value\n        order.remove(key)\n        order.addLast(key)\n        while (order.size > maxSize) {\n            val oldest = order.removeFirst()\n            map.remove(oldest)\n        }\n    }\n\n    fun contains(key: K): Boolean = map.containsKey(key)\n}\n"
  },
  {
    "path": "feature/search/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/search/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n\n                implementation(libs.kotlinx.coroutines.core)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/search/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/ProgrammingLanguage.kt",
    "content": "package zed.rainxch.domain.model\n\nenum class ProgrammingLanguage(\n    val queryValue: String?,\n) {\n    All(null),\n    Kotlin(\"kotlin\"),\n    Java(\"java\"),\n    JavaScript(\"javascript\"),\n    TypeScript(\"typescript\"),\n    Python(\"python\"),\n    Swift(\"swift\"),\n    Rust(\"rust\"),\n    Go(\"go\"),\n    CSharp(\"c#\"),\n    CPlusPlus(\"c++\"),\n    C(\"c\"),\n    Dart(\"dart\"),\n    Ruby(\"ruby\"),\n    PHP(\"php\"),\n    ;\n\n    companion object {\n        fun fromLanguageString(lang: String?): ProgrammingLanguage {\n            if (lang == null) return All\n            return entries.find {\n                it.queryValue?.equals(lang, ignoreCase = true) == true\n            } ?: All\n        }\n    }\n}\n"
  },
  {
    "path": "feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt",
    "content": "package zed.rainxch.domain.model\n\nenum class SortBy {\n    MostStars,\n    MostForks,\n    BestMatch,\n    ;\n\n    fun toGithubSortParam(): String? =\n        when (this) {\n            MostStars -> \"stars\"\n            MostForks -> \"forks\"\n            BestMatch -> null\n        }\n}\n"
  },
  {
    "path": "feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortOrder.kt",
    "content": "package zed.rainxch.domain.model\n\nenum class SortOrder {\n    Descending,\n    Ascending,\n    ;\n\n    fun toGithubParam(): String =\n        when (this) {\n            Descending -> \"desc\"\n            Ascending -> \"asc\"\n        }\n}\n"
  },
  {
    "path": "feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt",
    "content": "package zed.rainxch.domain.repository\n\nimport kotlinx.coroutines.flow.Flow\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories\nimport zed.rainxch.domain.model.ProgrammingLanguage\nimport zed.rainxch.domain.model.SortBy\nimport zed.rainxch.domain.model.SortOrder\n\ninterface SearchRepository {\n    fun searchRepositories(\n        query: String,\n        platform: DiscoveryPlatform,\n        language: ProgrammingLanguage,\n        sortBy: SortBy,\n        sortOrder: SortOrder,\n        page: Int,\n    ): Flow<PaginatedDiscoveryRepositories>\n}\n"
  },
  {
    "path": "feature/search/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/search/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.search.domain)\n\n                implementation(libs.liquid)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n\n                implementation(libs.kotlinx.collections.immutable)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt",
    "content": "package zed.rainxch.search.presentation\n\nimport zed.rainxch.core.presentation.model.GithubRepoSummaryUi\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi\nimport zed.rainxch.search.presentation.model.SearchPlatformUi\nimport zed.rainxch.search.presentation.model.SortByUi\nimport zed.rainxch.search.presentation.model.SortOrderUi\n\nsealed interface SearchAction {\n    data class OnSearchChange(\n        val query: String,\n    ) : SearchAction\n\n    data class OnPlatformTypeSelected(\n        val searchPlatform: SearchPlatformUi,\n    ) : SearchAction\n\n    data class OnLanguageSelected(\n        val language: ProgrammingLanguageUi,\n    ) : SearchAction\n\n    data class OnSortBySelected(\n        val sortBy: SortByUi,\n    ) : SearchAction\n\n    data class OnSortOrderSelected(\n        val sortOrder: SortOrderUi,\n    ) : SearchAction\n\n    data class OnRepositoryClick(\n        val repository: GithubRepoSummaryUi,\n    ) : SearchAction\n\n    data class OnRepositoryDeveloperClick(\n        val username: String,\n    ) : SearchAction\n\n    data class OnShareClick(\n        val repo: GithubRepoSummaryUi,\n    ) : SearchAction\n\n    data class OpenGithubLink(\n        val owner: String,\n        val repo: String,\n    ) : SearchAction\n\n    data object OnSearchImeClick : SearchAction\n\n    data object OnNavigateBackClick : SearchAction\n\n    data object LoadMore : SearchAction\n\n    data object OnClearClick : SearchAction\n\n    data object Retry : SearchAction\n\n    data object OnToggleLanguageSheetVisibility : SearchAction\n\n    data object OnToggleSortByDialogVisibility : SearchAction\n\n    data object OnFabClick : SearchAction\n\n    data object DismissClipboardBanner : SearchAction\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchEvent.kt",
    "content": "package zed.rainxch.search.presentation\n\nsealed interface SearchEvent {\n    data class OnMessage(\n        val message: String,\n    ) : SearchEvent\n\n    data class NavigateToRepo(\n        val owner: String,\n        val repo: String,\n    ) : SearchEvent\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt",
    "content": "package zed.rainxch.search.presentation\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid\nimport androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells\nimport androidx.compose.foundation.lazy.staggeredgrid.items\nimport androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.OpenInNew\nimport androidx.compose.material.icons.automirrored.filled.Sort\nimport androidx.compose.material.icons.filled.Clear\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Link\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.outlined.KeyboardArrowDown\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport io.github.fletchmckee.liquid.liquefiable\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.core.presentation.components.RepositoryCard\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight\nimport zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.ObserveAsEvents\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.search.presentation.components.LanguageFilterBottomSheet\nimport zed.rainxch.search.presentation.components.SortByBottomSheet\nimport zed.rainxch.search.presentation.model.ParsedGithubLink\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi\nimport zed.rainxch.search.presentation.model.SearchPlatformUi\nimport zed.rainxch.search.presentation.model.SortByUi\nimport zed.rainxch.search.presentation.utils.label\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SearchRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToDetails: (repoId: Long) -> Unit,\n    onNavigateToDetailsFromLink: (owner: String, repo: String) -> Unit,\n    onNavigateToDeveloperProfile: (username: String) -> Unit,\n    viewModel: SearchViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    val scope = rememberCoroutineScope()\n    val snackbarHost = remember { SnackbarHostState() }\n\n    ObserveAsEvents(viewModel.events) { event ->\n        when (event) {\n            is SearchEvent.OnMessage -> {\n                scope.launch {\n                    snackbarHost.showSnackbar(event.message)\n                }\n            }\n\n            is SearchEvent.NavigateToRepo -> {\n                onNavigateToDetailsFromLink(event.owner, event.repo)\n            }\n        }\n    }\n\n    SearchScreen(\n        state = state,\n        snackbarHost = snackbarHost,\n        onAction = { action ->\n            when (action) {\n                is SearchAction.OnRepositoryClick -> {\n                    onNavigateToDetails(action.repository.id)\n                }\n\n                SearchAction.OnNavigateBackClick -> {\n                    onNavigateBack()\n                }\n\n                is SearchAction.OnRepositoryDeveloperClick -> {\n                    onNavigateToDeveloperProfile(action.username)\n                }\n\n                else -> {\n                    viewModel.onAction(action)\n                }\n            }\n        },\n    )\n\n    if (state.isLanguageSheetVisible) {\n        LanguageFilterBottomSheet(\n            selectedLanguage = state.selectedLanguage,\n            onLanguageSelected = { language ->\n                viewModel.onAction(SearchAction.OnLanguageSelected(language))\n            },\n            onDismissRequest = {\n                viewModel.onAction(SearchAction.OnToggleLanguageSheetVisibility)\n            },\n        )\n    }\n\n    if (state.isSortByDialogVisible) {\n        SortByBottomSheet(\n            selectedSortBy = state.selectedSortBy,\n            selectedSortOrder = state.selectedSortOrder,\n            onSortBySelected = { sortBy ->\n                viewModel.onAction(SearchAction.OnSortBySelected(sortBy))\n            },\n            onSortOrderSelected = { sortOrder ->\n                viewModel.onAction(SearchAction.OnSortOrderSelected(sortOrder))\n            },\n            onDismissRequest = {\n                viewModel.onAction(SearchAction.OnToggleSortByDialogVisibility)\n            },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SearchScreen(\n    state: SearchState,\n    snackbarHost: SnackbarHostState,\n    onAction: (SearchAction) -> Unit,\n) {\n    val focusRequester = remember { FocusRequester() }\n    val listState = rememberLazyStaggeredGridState()\n    val liquidState = LocalBottomNavigationLiquid.current\n    val bottomNavHeight = LocalBottomNavigationHeight.current\n\n    val shouldLoadMore by remember {\n        derivedStateOf {\n            val layoutInfo = listState.layoutInfo\n            val totalItems = layoutInfo.totalItemsCount\n            val visibleItems = layoutInfo.visibleItemsInfo\n\n            if (totalItems == 0 ||\n                state.isLoadingMore ||\n                state.isLoading ||\n                !state.hasMorePages\n            ) {\n                return@derivedStateOf false\n            }\n\n            val lastVisibleItem = visibleItems.lastOrNull() ?: return@derivedStateOf false\n            val viewportEndOffset = layoutInfo.viewportEndOffset\n\n            val hasEmptySpaceAtBottom =\n                lastVisibleItem.index == totalItems - 1 &&\n                    lastVisibleItem.offset.y + lastVisibleItem.size.height < viewportEndOffset\n\n            val threshold = (totalItems * 0.8f).toInt()\n            val isNearEnd = lastVisibleItem.index >= threshold\n\n            isNearEnd || hasEmptySpaceAtBottom\n        }\n    }\n\n    val currentOnAction by rememberUpdatedState(onAction)\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) {\n            currentOnAction(SearchAction.LoadMore)\n        }\n    }\n\n    LaunchedEffect(listState.layoutInfo.totalItemsCount, listState.layoutInfo.viewportEndOffset) {\n        val layoutInfo = listState.layoutInfo\n        val visibleItems = layoutInfo.visibleItemsInfo\n        val lastVisible = visibleItems.lastOrNull()\n\n        if (lastVisible != null &&\n            layoutInfo.totalItemsCount > 0 &&\n            !state.isLoadingMore &&\n            !state.isLoading &&\n            state.hasMorePages\n        ) {\n            val hasEmptySpace =\n                lastVisible.index == layoutInfo.totalItemsCount - 1 &&\n                    lastVisible.offset.y + lastVisible.size.height < layoutInfo.viewportEndOffset\n\n            if (hasEmptySpace) {\n                delay(100)\n                currentOnAction(SearchAction.LoadMore)\n            }\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (state.query.isEmpty()) {\n            focusRequester.requestFocus()\n        }\n    }\n\n    Scaffold(\n        topBar = {\n            SearchTopbar(\n                onAction = onAction,\n                state = state,\n                focusRequester = focusRequester,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(\n                hostState = snackbarHost,\n                modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp),\n            )\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                onClick = {\n                    onAction(SearchAction.OnFabClick)\n                },\n                modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp),\n            ) {\n                Row(\n                    modifier = Modifier.padding(horizontal = 12.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Link,\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp),\n                    )\n\n                    Text(\n                        text = stringResource(Res.string.open_github_link),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                }\n            }\n        },\n        containerColor = MaterialTheme.colorScheme.background,\n        modifier = Modifier.then(\n            if (state.isLiquidGlassEnabled) {\n                Modifier.liquefiable(liquidState)\n            } else {\n                Modifier\n            },\n        ),\n    ) { innerPadding ->\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding)\n                    .padding(horizontal = 16.dp),\n        ) {\n            // Clipboard banner\n            AnimatedVisibility(\n                visible = state.isClipboardBannerVisible && state.clipboardLinks.isNotEmpty(),\n                enter = slideInVertically() + fadeIn(),\n                exit = slideOutVertically() + fadeOut(),\n            ) {\n                ClipboardBanner(\n                    links = state.clipboardLinks,\n                    onOpenLink = { link ->\n                        onAction(SearchAction.OpenGithubLink(link.owner, link.repo))\n                    },\n                    onDismiss = {\n                        onAction(SearchAction.DismissClipboardBanner)\n                    },\n                )\n            }\n\n            // Detected links from search query\n            AnimatedVisibility(\n                visible = state.detectedLinks.isNotEmpty(),\n                enter = slideInVertically() + fadeIn(),\n                exit = slideOutVertically() + fadeOut(),\n            ) {\n                DetectedLinksSection(\n                    links = state.detectedLinks,\n                    onOpenLink = { link ->\n                        onAction(SearchAction.OpenGithubLink(link.owner, link.repo))\n                    },\n                )\n            }\n\n            LazyRow(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                items(SearchPlatformUi.entries) { sortBy ->\n                    FilterChip(\n                        selected = state.selectedSearchPlatform == sortBy,\n                        label = {\n                            Text(\n                                text = sortBy.name.lowercase().replaceFirstChar { it.uppercase() },\n                                style = MaterialTheme.typography.titleSmall,\n                                fontWeight = FontWeight.Medium,\n                                color = MaterialTheme.colorScheme.onBackground,\n                            )\n                        },\n                        onClick = {\n                            onAction(SearchAction.OnPlatformTypeSelected(sortBy))\n                        },\n                    )\n                }\n            }\n\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .horizontalScroll(rememberScrollState()),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(6.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = stringResource(Res.string.language_label),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        fontWeight = FontWeight.Medium,\n                    )\n\n                    FilterChip(\n                        selected = state.selectedLanguage != ProgrammingLanguageUi.All,\n                        onClick = {\n                            onAction(SearchAction.OnToggleLanguageSheetVisibility)\n                        },\n                        label = {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(4.dp),\n                            ) {\n                                Text(\n                                    text = stringResource(state.selectedLanguage.label()),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    fontWeight = FontWeight.Medium,\n                                )\n                                Icon(\n                                    imageVector = Icons.Outlined.KeyboardArrowDown,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                )\n                            }\n                        },\n                    )\n\n                    if (state.selectedLanguage != ProgrammingLanguageUi.All) {\n                        IconButton(\n                            onClick = {\n                                onAction(SearchAction.OnLanguageSelected(ProgrammingLanguageUi.All))\n                            },\n                            modifier = Modifier.size(32.dp),\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.Close,\n                                contentDescription = null,\n                                modifier = Modifier.size(18.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(6.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = stringResource(Res.string.sort_label),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        fontWeight = FontWeight.Medium,\n                    )\n\n                    FilterChip(\n                        selected = state.selectedSortBy != SortByUi.BestMatch,\n                        onClick = {\n                            onAction(SearchAction.OnToggleSortByDialogVisibility)\n                        },\n                        label = {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(4.dp),\n                            ) {\n                                Icon(\n                                    imageVector = Icons.AutoMirrored.Filled.Sort,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                )\n                                Text(\n                                    text = stringResource(state.selectedSortBy.label()),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    fontWeight = FontWeight.Medium,\n                                )\n                                Icon(\n                                    imageVector = Icons.Outlined.KeyboardArrowDown,\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                )\n                            }\n                        },\n                    )\n                }\n            }\n\n            Spacer(Modifier.height(6.dp))\n\n            if (state.totalCount != null) {\n                Text(\n                    text =\n                        stringResource(\n                            Res.string.results_found,\n                            state.totalCount,\n                        ),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.outline,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(bottom = 6.dp),\n                )\n            }\n\n            val visibleRepos by remember(state.repositories, state.isHideSeenEnabled, state.seenRepoIds) {\n                derivedStateOf {\n                    if (state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty()) {\n                        state.repositories.filter { it.repository.id !in state.seenRepoIds }\n                    } else {\n                        state.repositories\n                    }\n                }\n            }\n\n            Box(Modifier.fillMaxSize()) {\n                if (state.isLoading && state.repositories.isEmpty()) {\n                    Box(\n                        modifier = Modifier.fillMaxSize().imePadding(),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        CircularWavyProgressIndicator()\n                    }\n                }\n\n                if (state.errorMessage != null && state.repositories.isEmpty()) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                            Text(\n                                text = state.errorMessage,\n                            )\n\n                            Spacer(Modifier.height(8.dp))\n\n                            GithubStoreButton(\n                                text = stringResource(Res.string.retry),\n                                onClick = {\n                                    onAction(SearchAction.Retry)\n                                },\n                            )\n                        }\n                    }\n                }\n\n                if (visibleRepos.isNotEmpty()) {\n                    LazyVerticalStaggeredGrid(\n                        state = listState,\n                        columns = StaggeredGridCells.Adaptive(350.dp),\n                        verticalItemSpacing = 12.dp,\n                        horizontalArrangement = Arrangement.spacedBy(12.dp),\n                        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp),\n                        modifier =\n                            Modifier\n                                .fillMaxSize()\n                                .then(\n                                    if (state.isLiquidGlassEnabled) {\n                                        Modifier.liquefiable(liquidState)\n                                    } else {\n                                        Modifier\n                                    },\n                                ),\n                    ) {\n                        items(\n                            items = visibleRepos,\n                            key = { it.repository.id },\n                        ) { discoveryRepository ->\n                            RepositoryCard(\n                                discoveryRepositoryUi = discoveryRepository,\n                                onClick = {\n                                    onAction(SearchAction.OnRepositoryClick(discoveryRepository.repository))\n                                },\n                                onDeveloperClick = { username ->\n                                    onAction(SearchAction.OnRepositoryDeveloperClick(username))\n                                },\n                                onShareClick = {\n                                    onAction(SearchAction.OnShareClick(discoveryRepository.repository))\n                                },\n                                modifier =\n                                    Modifier\n                                        .animateItem()\n                                        .then(\n                                            if (state.isLiquidGlassEnabled) {\n                                                Modifier.liquefiable(liquidState)\n                                            } else {\n                                                Modifier\n                                            },\n                                        ),\n                            )\n                        }\n\n                        item {\n                            if (state.isLoadingMore) {\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .padding(16.dp),\n                                    contentAlignment = Alignment.Center,\n                                ) {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ClipboardBanner(\n    links: ImmutableList<ParsedGithubLink>,\n    onOpenLink: (ParsedGithubLink) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    Card(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .padding(bottom = 8.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.secondaryContainer,\n            ),\n        shape = RoundedCornerShape(12.dp),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(12.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Text(\n                    text = stringResource(Res.string.clipboard_link_detected),\n                    style = MaterialTheme.typography.labelMedium,\n                    color = MaterialTheme.colorScheme.onSecondaryContainer,\n                    fontWeight = FontWeight.Medium,\n                )\n\n                IconButton(\n                    onClick = onDismiss,\n                    modifier = Modifier.size(24.dp),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Close,\n                        contentDescription = stringResource(Res.string.dismiss),\n                        modifier = Modifier.size(16.dp),\n                        tint = MaterialTheme.colorScheme.onSecondaryContainer,\n                    )\n                }\n            }\n\n            Spacer(Modifier.height(4.dp))\n\n            links.forEach { link ->\n                Row(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clip(RoundedCornerShape(8.dp))\n                            .clickable { onOpenLink(link) }\n                            .padding(vertical = 6.dp, horizontal = 4.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Link,\n                        contentDescription = null,\n                        modifier = Modifier.size(16.dp),\n                        tint = MaterialTheme.colorScheme.onSecondaryContainer,\n                    )\n                    Text(\n                        text = \"${link.owner}/${link.repo}\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSecondaryContainer,\n                        fontWeight = FontWeight.Medium,\n                        modifier = Modifier.weight(1f),\n                    )\n                    Icon(\n                        imageVector = Icons.AutoMirrored.Filled.OpenInNew,\n                        contentDescription = stringResource(Res.string.open_in_app),\n                        modifier = Modifier.size(16.dp),\n                        tint = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun DetectedLinksSection(\n    links: ImmutableList<ParsedGithubLink>,\n    onOpenLink: (ParsedGithubLink) -> Unit,\n) {\n    Column(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .padding(bottom = 8.dp),\n    ) {\n        Text(\n            text = stringResource(Res.string.detected_links),\n            style = MaterialTheme.typography.labelMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            fontWeight = FontWeight.Medium,\n            modifier = Modifier.padding(bottom = 4.dp),\n        )\n\n        links.forEach { link ->\n            Card(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(vertical = 2.dp),\n                onClick = { onOpenLink(link) },\n                colors =\n                    CardDefaults.cardColors(\n                        containerColor = MaterialTheme.colorScheme.primaryContainer,\n                    ),\n                shape = RoundedCornerShape(8.dp),\n            ) {\n                Row(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 12.dp, vertical = 10.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Link,\n                        contentDescription = null,\n                        modifier = Modifier.size(18.dp),\n                        tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                    )\n                    Text(\n                        text = \"${link.owner}/${link.repo}\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onPrimaryContainer,\n                        fontWeight = FontWeight.Medium,\n                        modifier = Modifier.weight(1f),\n                    )\n                    Text(\n                        text = stringResource(Res.string.open_in_app),\n                        style = MaterialTheme.typography.labelMedium,\n                        color = MaterialTheme.colorScheme.primary,\n                        fontWeight = FontWeight.Bold,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SearchTopbar(\n    onAction: (SearchAction) -> Unit,\n    state: SearchState,\n    focusRequester: FocusRequester,\n) {\n    Row(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .statusBarsPadding()\n                .padding(horizontal = 8.dp, vertical = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        TextField(\n            value = state.query,\n            onValueChange = { value ->\n                onAction(SearchAction.OnSearchChange(value))\n            },\n            leadingIcon = {\n                Icon(\n                    imageVector = Icons.Default.Search,\n                    contentDescription = null,\n                    modifier = Modifier.size(20.dp),\n                )\n            },\n            trailingIcon = {\n                IconButton(\n                    onClick = {\n                        onAction(SearchAction.OnClearClick)\n                    },\n                    modifier =\n                        Modifier\n                            .size(24.dp)\n                            .clip(CircleShape),\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Clear,\n                        contentDescription = null,\n                    )\n                }\n            },\n            placeholder = {\n                Text(\n                    text = stringResource(Res.string.search_repositories_hint),\n                    style = MaterialTheme.typography.bodyLarge,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    softWrap = false,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            },\n            textStyle =\n                MaterialTheme.typography.bodyLarge.copy(\n                    color = MaterialTheme.colorScheme.onSurface,\n                ),\n            keyboardOptions =\n                KeyboardOptions(\n                    keyboardType = KeyboardType.Text,\n                    imeAction = ImeAction.Search,\n                ),\n            keyboardActions =\n                KeyboardActions(\n                    onSearch = { onAction(SearchAction.OnSearchImeClick) },\n                ),\n            singleLine = true,\n            colors =\n                TextFieldDefaults.colors(\n                    focusedIndicatorColor = Color.Transparent,\n                    unfocusedIndicatorColor = Color.Transparent,\n                    focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer,\n                    unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer,\n                ),\n            shape = CircleShape,\n            modifier =\n                Modifier\n                    .weight(1f)\n                    .focusRequester(focusRequester),\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    GithubStoreTheme {\n        SearchScreen(\n            state = SearchState(),\n            snackbarHost = SnackbarHostState(),\n            onAction = {},\n        )\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt",
    "content": "package zed.rainxch.search.presentation\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport zed.rainxch.core.presentation.model.DiscoveryRepositoryUi\nimport zed.rainxch.search.presentation.model.ParsedGithubLink\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi\nimport zed.rainxch.search.presentation.model.SearchPlatformUi\nimport zed.rainxch.search.presentation.model.SortByUi\nimport zed.rainxch.search.presentation.model.SortOrderUi\n\ndata class SearchState(\n    val query: String = \"\",\n    val repositories: ImmutableList<DiscoveryRepositoryUi> = persistentListOf(),\n    val selectedSearchPlatform: SearchPlatformUi = SearchPlatformUi.All,\n    val selectedSortBy: SortByUi = SortByUi.BestMatch,\n    val selectedSortOrder: SortOrderUi = SortOrderUi.Descending,\n    val selectedLanguage: ProgrammingLanguageUi = ProgrammingLanguageUi.All,\n    val isLoading: Boolean = false,\n    val isLiquidGlassEnabled: Boolean = true,\n    val isHideSeenEnabled: Boolean = false,\n    val seenRepoIds: Set<Long> = emptySet(),\n    val isLoadingMore: Boolean = false,\n    val errorMessage: String? = null,\n    val hasMorePages: Boolean = true,\n    val totalCount: Int? = null,\n    val isLanguageSheetVisible: Boolean = false,\n    val isSortByDialogVisible: Boolean = false,\n    val detectedLinks: ImmutableList<ParsedGithubLink> = persistentListOf(),\n    val clipboardLinks: ImmutableList<ParsedGithubLink> = persistentListOf(),\n    val isClipboardBannerVisible: Boolean = false,\n    val autoDetectClipboardEnabled: Boolean = true,\n)\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt",
    "content": "package zed.rainxch.search.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.collections.immutable.persistentListOf\nimport kotlinx.collections.immutable.toImmutableList\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.core.domain.logging.GitHubStoreLogger\nimport zed.rainxch.core.domain.model.Platform\nimport zed.rainxch.core.domain.model.RateLimitException\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.core.domain.repository.InstalledAppsRepository\nimport zed.rainxch.core.domain.repository.SeenReposRepository\nimport zed.rainxch.core.domain.repository.StarredRepository\nimport zed.rainxch.core.domain.repository.TweaksRepository\nimport zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase\nimport zed.rainxch.core.domain.utils.ClipboardHelper\nimport zed.rainxch.core.domain.utils.ShareManager\nimport zed.rainxch.core.presentation.model.DiscoveryRepositoryUi\nimport zed.rainxch.core.presentation.utils.toUi\nimport zed.rainxch.domain.repository.SearchRepository\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.failed_to_share_link\nimport zed.rainxch.githubstore.core.presentation.res.link_copied_to_clipboard\nimport zed.rainxch.githubstore.core.presentation.res.no_github_link_in_clipboard\nimport zed.rainxch.githubstore.core.presentation.res.no_repositories_found\nimport zed.rainxch.githubstore.core.presentation.res.search_failed\nimport zed.rainxch.search.presentation.mappers.toDomain\nimport zed.rainxch.search.presentation.utils.isEntirelyGithubUrls\nimport zed.rainxch.search.presentation.utils.parseGithubUrls\n\nclass SearchViewModel(\n    private val searchRepository: SearchRepository,\n    private val installedAppsRepository: InstalledAppsRepository,\n    private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase,\n    private val favouritesRepository: FavouritesRepository,\n    private val starredRepository: StarredRepository,\n    private val logger: GitHubStoreLogger,\n    private val shareManager: ShareManager,\n    private val platform: Platform,\n    private val clipboardHelper: ClipboardHelper,\n    private val tweaksRepository: TweaksRepository,\n    private val seenReposRepository: SeenReposRepository,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n    private var currentSearchJob: Job? = null\n    private var currentPage = 1\n    private var searchDebounceJob: Job? = null\n\n    companion object {\n        private const val MIN_QUERY_LENGTH = 3\n        private const val DEBOUNCE_MS = 800L\n    }\n\n    private val _state = MutableStateFlow(SearchState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    syncSystemState()\n\n                    observeInstalledApps()\n                    observeFavouriteApps()\n                    observeStarredRepos()\n                    observeLiquidGlassEnabled()\n                    observeSeenRepos()\n                    observeHideSeenEnabled()\n                    observeClipboardSetting()\n                    checkClipboardForLinks()\n\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = SearchState(),\n            )\n\n    private fun observeLiquidGlassEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getLiquidGlassEnabled().collect { enabled ->\n                _state.update {\n                    it.copy(\n                        isLiquidGlassEnabled = enabled,\n                    )\n                }\n            }\n        }\n    }\n\n    private fun observeSeenRepos() {\n        viewModelScope.launch {\n            seenReposRepository.getAllSeenRepoIds().collect { ids ->\n                _state.update { current ->\n                    current.copy(\n                        seenRepoIds = ids,\n                        repositories =\n                            current.repositories\n                                .map { repo ->\n                                    repo.copy(isSeen = repo.repository.id in ids)\n                                }.toImmutableList(),\n                    )\n                }\n            }\n        }\n    }\n\n    private fun observeHideSeenEnabled() {\n        viewModelScope.launch {\n            tweaksRepository.getHideSeenEnabled().collect { enabled ->\n                _state.update { it.copy(isHideSeenEnabled = enabled) }\n            }\n        }\n    }\n\n    private val _events = Channel<SearchEvent>()\n    val events = _events.receiveAsFlow()\n\n    private fun syncSystemState() {\n        viewModelScope.launch {\n            try {\n                val result = syncInstalledAppsUseCase()\n                if (result.isFailure) {\n                    logger.warn(\"Initial sync had issues: ${result.exceptionOrNull()?.message}\")\n                }\n            } catch (e: Exception) {\n                logger.error(\"Initial sync failed: ${e.message}\")\n            }\n        }\n    }\n\n    private fun observeClipboardSetting() {\n        viewModelScope.launch {\n            tweaksRepository.getAutoDetectClipboardLinks().collect { enabled ->\n                _state.update { current ->\n                    current.copy(\n                        autoDetectClipboardEnabled = enabled,\n                        clipboardLinks = if (enabled) current.clipboardLinks else persistentListOf(),\n                        isClipboardBannerVisible = if (enabled) current.isClipboardBannerVisible else false,\n                    )\n                }\n                if (enabled) checkClipboardForLinks()\n            }\n        }\n    }\n\n    private fun checkClipboardForLinks() {\n        viewModelScope.launch {\n            val enabled = tweaksRepository.getAutoDetectClipboardLinks().first()\n            if (!enabled) return@launch\n\n            try {\n                val clipText = clipboardHelper.getText() ?: return@launch\n                val links = parseGithubUrls(clipText).toImmutableList()\n                if (links.isNotEmpty()) {\n                    _state.update {\n                        it.copy(\n                            clipboardLinks = links,\n                            isClipboardBannerVisible = true,\n                        )\n                    }\n                }\n            } catch (e: Exception) {\n                logger.debug(\"Failed to read clipboard: ${e.message}\")\n            }\n        }\n    }\n\n    private fun observeInstalledApps() {\n        viewModelScope.launch {\n            installedAppsRepository\n                .getAllInstalledApps()\n                .collect { installedApps ->\n                    val installedMap = installedApps.associateBy { it.repoId }\n                    _state.update { current ->\n                        current.copy(\n                            repositories =\n                                current.repositories\n                                    .map { searchRepo ->\n                                        val app = installedMap[searchRepo.repository.id]\n                                        searchRepo.copy(\n                                            isInstalled = app != null,\n                                            isUpdateAvailable = app?.isUpdateAvailable ?: false,\n                                        )\n                                    }.toImmutableList(),\n                        )\n                    }\n                }\n        }\n    }\n\n    private fun observeFavouriteApps() {\n        viewModelScope.launch {\n            favouritesRepository.getAllFavorites().collect { favoriteRepos ->\n                val installedMap = favoriteRepos.associateBy { it.repoId }\n                _state.update { current ->\n                    current.copy(\n                        repositories =\n                            current.repositories\n                                .map { searchRepo ->\n                                    val app = installedMap[searchRepo.repository.id]\n                                    searchRepo.copy(\n                                        isFavourite = app != null,\n                                    )\n                                }.toImmutableList(),\n                    )\n                }\n            }\n        }\n    }\n\n    private fun observeStarredRepos() {\n        viewModelScope.launch {\n            starredRepository.getAllStarred().collect { starredRepos ->\n                val installedMap = starredRepos.associateBy { it.repoId }\n                _state.update { current ->\n                    current.copy(\n                        repositories =\n                            current.repositories\n                                .map { searchRepo ->\n                                    val app = installedMap[searchRepo.repository.id]\n                                    searchRepo.copy(isStarred = app != null)\n                                }.toImmutableList(),\n                    )\n                }\n            }\n        }\n    }\n\n    private fun performSearch(isInitial: Boolean = false) {\n        val query = _state.value.query.trim()\n        if (query.isBlank() || query.length < MIN_QUERY_LENGTH) {\n            if (query.isBlank()) {\n                _state.update {\n                    it.copy(\n                        isLoading = false,\n                        isLoadingMore = false,\n                        repositories = persistentListOf(),\n                        errorMessage = null,\n                        totalCount = null,\n                    )\n                }\n            }\n            return\n        }\n\n        if (isInitial) {\n            currentSearchJob?.cancel()\n            currentPage = 1\n        }\n\n        currentSearchJob =\n            viewModelScope.launch {\n                _state.update {\n                    it.copy(\n                        isLoading = isInitial,\n                        isLoadingMore = !isInitial,\n                        errorMessage = null,\n                        repositories =\n                            if (isInitial) {\n                                persistentListOf()\n                            } else {\n                                it.repositories\n                            },\n                        totalCount = if (isInitial) null else it.totalCount,\n                    )\n                }\n\n                try {\n                    val installedMap =\n                        installedAppsRepository\n                            .getAllInstalledApps()\n                            .first()\n                            .associateBy { it.repoId }\n                    val favoritesMap =\n                        favouritesRepository\n                            .getAllFavorites()\n                            .first()\n                            .associateBy { it.repoId }\n                    val starredReposMap =\n                        starredRepository\n                            .getAllStarred()\n                            .first()\n                            .associateBy { it.repoId }\n\n                    searchRepository\n                        .searchRepositories(\n                            query = _state.value.query,\n                            platform = _state.value.selectedSearchPlatform.toDomain(),\n                            language = _state.value.selectedLanguage.toDomain(),\n                            sortBy = _state.value.selectedSortBy.toDomain(),\n                            sortOrder = _state.value.selectedSortOrder.toDomain(),\n                            page = currentPage,\n                        ).collect { paginatedRepos ->\n                            currentPage = paginatedRepos.nextPageIndex\n\n                            val seenIds = _state.value.seenRepoIds\n\n                            val newReposWithStatus =\n                                paginatedRepos.repos.map { repo ->\n                                    val app = installedMap[repo.id]\n                                    val favourite = favoritesMap[repo.id]\n                                    val starred = starredReposMap[repo.id]\n\n                                    DiscoveryRepositoryUi(\n                                        isInstalled = app != null,\n                                        isFavourite = favourite != null,\n                                        isStarred = starred != null,\n                                        isSeen = repo.id in seenIds,\n                                        isUpdateAvailable = app?.isUpdateAvailable ?: false,\n                                        repository = repo.toUi(),\n                                    )\n                                }\n\n                            _state.update { currentState ->\n                                val mergedMap = LinkedHashMap<Long, DiscoveryRepositoryUi>()\n\n                                currentState.repositories.forEach { r ->\n                                    mergedMap[r.repository.id] = r\n                                }\n\n                                newReposWithStatus.forEach { r ->\n                                    val existing = mergedMap[r.repository.id]\n                                    if (existing == null) {\n                                        mergedMap[r.repository.id] = r\n                                    } else {\n                                        mergedMap[r.repository.id] =\n                                            existing.copy(\n                                                isInstalled = r.isInstalled,\n                                                isUpdateAvailable = r.isUpdateAvailable,\n                                                isFavourite = r.isFavourite,\n                                                isStarred = r.isStarred,\n                                                repository = r.repository,\n                                            )\n                                    }\n                                }\n\n                                val allRepos = mergedMap.values.toImmutableList()\n\n                                currentState.copy(\n                                    repositories = allRepos,\n                                    hasMorePages = paginatedRepos.hasMore,\n                                    totalCount = allRepos.size,\n                                    errorMessage =\n                                        if (allRepos.isEmpty() && !paginatedRepos.hasMore) {\n                                            getString(Res.string.no_repositories_found)\n                                        } else {\n                                            null\n                                        },\n                                )\n                            }\n                        }\n\n                    _state.update {\n                        it.copy(isLoading = false, isLoadingMore = false)\n                    }\n                } catch (e: RateLimitException) {\n                    logger.debug(\"Rate limit exceeded: ${e.message}\")\n                    _state.update {\n                        it.copy(\n                            isLoading = false,\n                            isLoadingMore = false,\n                            errorMessage = e.message,\n                        )\n                    }\n                } catch (e: CancellationException) {\n                    logger.debug(\"Search cancelled (expected): ${e.message}\")\n                } catch (e: Exception) {\n                    logger.error(\"Search failed: ${e.message}\")\n                    _state.update {\n                        it.copy(\n                            isLoading = false,\n                            isLoadingMore = false,\n                            errorMessage = e.message ?: getString(Res.string.search_failed),\n                        )\n                    }\n                }\n            }\n    }\n\n    fun onAction(action: SearchAction) {\n        when (action) {\n            is SearchAction.OnPlatformTypeSelected -> {\n                if (_state.value.selectedSearchPlatform != action.searchPlatform) {\n                    _state.update {\n                        it.copy(selectedSearchPlatform = action.searchPlatform)\n                    }\n                    currentPage = 1\n                    searchDebounceJob?.cancel()\n                    performSearch(isInitial = true)\n                }\n            }\n\n            is SearchAction.OnLanguageSelected -> {\n                if (_state.value.selectedLanguage != action.language) {\n                    _state.update {\n                        it.copy(selectedLanguage = action.language)\n                    }\n                    currentPage = 1\n                    searchDebounceJob?.cancel()\n                    performSearch(isInitial = true)\n                }\n            }\n\n            is SearchAction.OnSearchChange -> {\n                val links = parseGithubUrls(action.query)\n                _state.update {\n                    it.copy(\n                        query = action.query,\n                        detectedLinks = links,\n                    )\n                }\n\n                searchDebounceJob?.cancel()\n\n                if (isEntirelyGithubUrls(action.query)) {\n                    currentSearchJob?.cancel()\n                    _state.update {\n                        it.copy(\n                            isLoading = false,\n                            isLoadingMore = false,\n                            errorMessage = null,\n                            repositories = persistentListOf(),\n                            totalCount = null,\n                        )\n                    }\n                    return\n                }\n\n                if (action.query.isBlank()) {\n                    _state.update {\n                        it.copy(\n                            repositories = persistentListOf(),\n                            isLoading = false,\n                            isLoadingMore = false,\n                            errorMessage = null,\n                            totalCount = null,\n                        )\n                    }\n                } else if (action.query.trim().length < MIN_QUERY_LENGTH) {\n                    currentSearchJob?.cancel()\n                    _state.update {\n                        it.copy(\n                            isLoading = false,\n                            isLoadingMore = false,\n                            errorMessage = null,\n                        )\n                    }\n                } else {\n                    searchDebounceJob =\n                        viewModelScope.launch {\n                            try {\n                                delay(DEBOUNCE_MS)\n                                currentPage = 1\n                                performSearch(isInitial = true)\n                            } catch (_: CancellationException) {\n                                logger.debug(\"Debounce cancelled (expected)\")\n                            }\n                        }\n                }\n            }\n\n            SearchAction.OnToggleLanguageSheetVisibility -> {\n                _state.update {\n                    it.copy(isLanguageSheetVisible = !it.isLanguageSheetVisible)\n                }\n            }\n\n            SearchAction.OnSearchImeClick -> {\n                if (_state.value.detectedLinks.isNotEmpty() && isEntirelyGithubUrls(_state.value.query)) {\n                    val link = _state.value.detectedLinks.first()\n                    viewModelScope.launch {\n                        _events.send(SearchEvent.NavigateToRepo(link.owner, link.repo))\n                    }\n                    return\n                }\n                searchDebounceJob?.cancel()\n                currentPage = 1\n                performSearch(isInitial = true)\n            }\n\n            is SearchAction.OnShareClick -> {\n                viewModelScope.launch {\n                    runCatching {\n                        shareManager.shareText(\"https://github-store.org/app?repo=${action.repo.fullName}\")\n                    }.onFailure { t ->\n                        logger.error(\"Failed to share link: ${t.message}\")\n                        _events.send(\n                            SearchEvent.OnMessage(getString(Res.string.failed_to_share_link)),\n                        )\n                        return@launch\n                    }\n\n                    if (platform != Platform.ANDROID) {\n                        _events.send(SearchEvent.OnMessage(getString(Res.string.link_copied_to_clipboard)))\n                    }\n                }\n            }\n\n            is SearchAction.OnSortBySelected -> {\n                if (_state.value.selectedSortBy != action.sortBy) {\n                    _state.update {\n                        it.copy(selectedSortBy = action.sortBy)\n                    }\n                    currentPage = 1\n                    searchDebounceJob?.cancel()\n                    performSearch(isInitial = true)\n                }\n            }\n\n            is SearchAction.OnSortOrderSelected -> {\n                if (_state.value.selectedSortOrder != action.sortOrder) {\n                    _state.update {\n                        it.copy(selectedSortOrder = action.sortOrder)\n                    }\n                    currentPage = 1\n                    searchDebounceJob?.cancel()\n                    performSearch(isInitial = true)\n                }\n            }\n\n            SearchAction.OnToggleSortByDialogVisibility -> {\n                _state.update {\n                    it.copy(isSortByDialogVisible = !it.isSortByDialogVisible)\n                }\n            }\n\n            SearchAction.LoadMore -> {\n                if (!_state.value.isLoadingMore && !_state.value.isLoading && _state.value.hasMorePages) {\n                    performSearch(isInitial = false)\n                }\n            }\n\n            SearchAction.Retry -> {\n                currentPage = 1\n                searchDebounceJob?.cancel()\n                performSearch(isInitial = true)\n            }\n\n            SearchAction.OnClearClick -> {\n                _state.update {\n                    it.copy(\n                        query = \"\",\n                        repositories = persistentListOf(),\n                        isLoading = false,\n                        isLoadingMore = false,\n                        errorMessage = null,\n                        totalCount = null,\n                        detectedLinks = persistentListOf(),\n                    )\n                }\n            }\n\n            is SearchAction.OpenGithubLink -> {\n                viewModelScope.launch {\n                    _events.send(SearchEvent.NavigateToRepo(action.owner, action.repo))\n                }\n            }\n\n            SearchAction.OnFabClick -> {\n                viewModelScope.launch {\n                    try {\n                        val clipText = clipboardHelper.getText()\n                        if (clipText.isNullOrBlank()) {\n                            _events.send(SearchEvent.OnMessage(getString(Res.string.no_github_link_in_clipboard)))\n                            return@launch\n                        }\n                        val links = parseGithubUrls(clipText)\n                        if (links.isEmpty()) {\n                            _events.send(SearchEvent.OnMessage(getString(Res.string.no_github_link_in_clipboard)))\n                            return@launch\n                        }\n                        if (links.size == 1) {\n                            _events.send(\n                                SearchEvent.NavigateToRepo(\n                                    links.first().owner,\n                                    links.first().repo,\n                                ),\n                            )\n                        } else {\n                            _state.update {\n                                it.copy(\n                                    query = clipText,\n                                    detectedLinks = links,\n                                    repositories = persistentListOf(),\n                                    totalCount = null,\n                                    isLoading = false,\n                                    isLoadingMore = false,\n                                    errorMessage = null,\n                                )\n                            }\n                        }\n                    } catch (e: Exception) {\n                        logger.error(\"Failed to read clipboard: ${e.message}\")\n                        _events.send(SearchEvent.OnMessage(getString(Res.string.no_github_link_in_clipboard)))\n                    }\n                }\n            }\n\n            SearchAction.DismissClipboardBanner -> {\n                _state.update {\n                    it.copy(isClipboardBannerVisible = false)\n                }\n            }\n\n            is SearchAction.OnRepositoryClick -> {\n                // Handled in composable\n            }\n\n            SearchAction.OnNavigateBackClick -> {\n                // Handled in composable\n            }\n\n            is SearchAction.OnRepositoryDeveloperClick -> {\n                // Handled in composable\n            }\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        currentSearchJob?.cancel()\n        searchDebounceJob?.cancel()\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/LanguageFilterBottomSheet.kt",
    "content": "package zed.rainxch.search.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.SheetState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi\nimport zed.rainxch.search.presentation.utils.label\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LanguageFilterBottomSheet(\n    selectedLanguage: ProgrammingLanguageUi,\n    onLanguageSelected: (ProgrammingLanguageUi) -> Unit,\n    onDismissRequest: () -> Unit,\n    sheetState: SheetState = rememberModalBottomSheetState(),\n) {\n    ModalBottomSheet(\n        onDismissRequest = onDismissRequest,\n        sheetState = sheetState,\n        containerColor = MaterialTheme.colorScheme.surface,\n        tonalElevation = 0.dp,\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp)\n                    .padding(bottom = 32.dp),\n        ) {\n            Text(\n                text = stringResource(Res.string.filter_by_language),\n                style = MaterialTheme.typography.titleLarge,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.onSurface,\n                modifier = Modifier.padding(bottom = 16.dp),\n            )\n\n            LazyVerticalGrid(\n                columns = GridCells.Fixed(2),\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalArrangement = Arrangement.spacedBy(8.dp),\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                items(ProgrammingLanguageUi.entries.toList()) { language ->\n                    FilterChip(\n                        selected = selectedLanguage == language,\n                        onClick = {\n                            onLanguageSelected(language)\n                            onDismissRequest()\n                        },\n                        label = {\n                            Text(\n                                text = stringResource(language.label()),\n                                style = MaterialTheme.typography.bodyMedium,\n                                fontWeight =\n                                    if (selectedLanguage == language) {\n                                        FontWeight.SemiBold\n                                    } else {\n                                        FontWeight.Normal\n                                    },\n                                modifier = Modifier.fillMaxWidth(),\n                                textAlign = TextAlign.Center,\n                            )\n                        },\n                        modifier = Modifier.fillMaxWidth(),\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt",
    "content": "package zed.rainxch.search.presentation.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.Res\nimport zed.rainxch.githubstore.core.presentation.res.close\nimport zed.rainxch.githubstore.core.presentation.res.sort_by\nimport zed.rainxch.search.presentation.model.SortByUi\nimport zed.rainxch.search.presentation.model.SortOrderUi\nimport zed.rainxch.search.presentation.utils.label\n\n@Composable\nfun SortByBottomSheet(\n    selectedSortBy: SortByUi,\n    selectedSortOrder: SortOrderUi,\n    onSortBySelected: (SortByUi) -> Unit,\n    onSortOrderSelected: (SortOrderUi) -> Unit,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    AlertDialog(\n        onDismissRequest = onDismissRequest,\n        confirmButton = {},\n        dismissButton = {\n            TextButton(onClick = onDismissRequest) {\n                Text(text = stringResource(Res.string.close))\n            }\n        },\n        title = {\n            Text(\n                text = stringResource(Res.string.sort_by),\n                style = MaterialTheme.typography.titleMedium,\n            )\n        },\n        text = {\n            Column(\n                modifier = modifier.fillMaxWidth(),\n                verticalArrangement = Arrangement.spacedBy(6.dp),\n            ) {\n                SortByUi.entries.forEach { option ->\n                    val isSelected = option == selectedSortBy\n                    TextButton(\n                        onClick = {\n                            onSortBySelected(option)\n                        },\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        Text(\n                            text = stringResource(option.label()) + if (isSelected) \"  ✓\" else \"\",\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,\n                        )\n                    }\n                }\n\n                HorizontalDivider()\n\n                Spacer(Modifier.height(4.dp))\n\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    SortOrderUi.entries.forEach { order ->\n                        FilterChip(\n                            selected = order == selectedSortOrder,\n                            onClick = { onSortOrderSelected(order) },\n                            label = {\n                                Text(\n                                    text = stringResource(order.label()),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                )\n                            },\n                        )\n                    }\n                }\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/mappers/PlatformLanguageMappers.kt",
    "content": "package zed.rainxch.search.presentation.mappers\n\nimport zed.rainxch.domain.model.ProgrammingLanguage\nimport zed.rainxch.domain.model.ProgrammingLanguage.*\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi\n\nfun ProgrammingLanguageUi.toDomain(): ProgrammingLanguage {\n    return when (this) {\n        ProgrammingLanguageUi.All -> All\n        ProgrammingLanguageUi.Kotlin -> Kotlin\n        ProgrammingLanguageUi.Java -> Java\n        ProgrammingLanguageUi.JavaScript -> JavaScript\n        ProgrammingLanguageUi.TypeScript -> TypeScript\n        ProgrammingLanguageUi.Python -> Python\n        ProgrammingLanguageUi.Swift -> Swift\n        ProgrammingLanguageUi.Rust -> Rust\n        ProgrammingLanguageUi.Go -> Go\n        ProgrammingLanguageUi.CSharp -> CSharp\n        ProgrammingLanguageUi.CPlusPlus -> CPlusPlus\n        ProgrammingLanguageUi.C -> C\n        ProgrammingLanguageUi.Dart -> Dart\n        ProgrammingLanguageUi.Ruby -> Ruby\n        ProgrammingLanguageUi.PHP -> PHP\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/mappers/SearchPlatformMappers.kt",
    "content": "package zed.rainxch.search.presentation.mappers\n\nimport zed.rainxch.core.domain.model.DiscoveryPlatform\nimport zed.rainxch.core.domain.model.DiscoveryPlatform.*\nimport zed.rainxch.search.presentation.model.SearchPlatformUi\n\nfun SearchPlatformUi.toDomain(): DiscoveryPlatform =\n    when (this) {\n        SearchPlatformUi.All -> All\n        SearchPlatformUi.Android -> Android\n        SearchPlatformUi.Windows -> Windows\n        SearchPlatformUi.Macos -> Macos\n        SearchPlatformUi.Linux -> Linux\n    }\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/mappers/SortByMappers.kt",
    "content": "package zed.rainxch.search.presentation.mappers\n\nimport zed.rainxch.domain.model.SortBy\nimport zed.rainxch.domain.model.SortBy.*\nimport zed.rainxch.search.presentation.model.SortByUi\n\nfun SortByUi.toDomain(): SortBy {\n    return when (this) {\n        SortByUi.MostStars -> MostStars\n        SortByUi.MostForks -> MostForks\n        SortByUi.BestMatch -> BestMatch\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/mappers/SortOrderMapper.kt",
    "content": "package zed.rainxch.search.presentation.mappers\n\nimport zed.rainxch.domain.model.SortOrder\nimport zed.rainxch.domain.model.SortOrder.*\nimport zed.rainxch.search.presentation.model.SortOrderUi\n\nfun SortOrderUi.toDomain() : SortOrder {\n    return when (this) {\n        SortOrderUi.Descending -> Descending\n        SortOrderUi.Ascending -> Ascending\n    }\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/model/ParsedGithubLink.kt",
    "content": "package zed.rainxch.search.presentation.model\n\ndata class ParsedGithubLink(\n    val owner: String,\n    val repo: String,\n    val fullUrl: String,\n)\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/model/ProgrammingLanguageUi.kt",
    "content": "package zed.rainxch.search.presentation.model\n\nenum class ProgrammingLanguageUi {\n    All,\n    Kotlin,\n    Java,\n    JavaScript,\n    TypeScript,\n    Python,\n    Swift,\n    Rust,\n    Go,\n    CSharp,\n    CPlusPlus,\n    C,\n    Dart,\n    Ruby,\n    PHP,\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/model/SearchPlatformUi.kt",
    "content": "package zed.rainxch.search.presentation.model\n\nenum class SearchPlatformUi {\n    All,\n    Android,\n    Windows,\n    Macos,\n    Linux,\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/model/SortByUi.kt",
    "content": "package zed.rainxch.search.presentation.model\n\nenum class SortByUi {\n    MostStars,\n    MostForks,\n    BestMatch,\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/model/SortOrderUi.kt",
    "content": "package zed.rainxch.search.presentation.model\n\nenum class SortOrderUi {\n    Descending,\n    Ascending,\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt",
    "content": "package zed.rainxch.search.presentation.utils\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.toImmutableList\nimport zed.rainxch.search.presentation.model.ParsedGithubLink\n\nprivate val GITHUB_URL_REGEX =\n    Regex(\n        \"\"\"(?<![A-Za-z0-9.-])(?:https?://)?(?:www\\.)?github\\.com/([a-zA-Z0-9\\-_.]+)/([a-zA-Z0-9\\-_.]+)\"\"\",\n    )\n\nfun parseGithubUrls(text: String): ImmutableList<ParsedGithubLink> =\n    GITHUB_URL_REGEX\n        .findAll(text)\n        .map { match ->\n            ParsedGithubLink(\n                owner = match.groupValues[1],\n                repo = match.groupValues[2].removeSuffix(\".git\"),\n                fullUrl = \"https://github.com/${match.groupValues[1]}/${match.groupValues[2].removeSuffix(\".git\")}\",\n            )\n        }.distinctBy { \"${it.owner}/${it.repo}\" }\n        .toImmutableList()\n\nfun isEntirelyGithubUrls(text: String): Boolean {\n    val stripped =\n        text\n            .replace(GITHUB_URL_REGEX, \"\")\n            .replace(Regex(\"\"\"[\\s,;]+\"\"\"), \"\")\n    return stripped.isEmpty() && parseGithubUrls(text).isNotEmpty()\n}\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/ProgrammingLanguageMapper.kt",
    "content": "package zed.rainxch.search.presentation.utils\n\nimport org.jetbrains.compose.resources.StringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi\nimport zed.rainxch.search.presentation.model.ProgrammingLanguageUi.*\n\nfun ProgrammingLanguageUi.label(): StringResource =\n    when (this) {\n        All -> Res.string.language_all\n        Kotlin -> Res.string.language_kotlin\n        Java -> Res.string.language_java\n        JavaScript -> Res.string.language_javascript\n        TypeScript -> Res.string.language_typescript\n        Python -> Res.string.language_python\n        Swift -> Res.string.language_swift\n        Rust -> Res.string.language_rust\n        Go -> Res.string.language_go\n        CSharp -> Res.string.language_csharp\n        CPlusPlus -> Res.string.language_cpp\n        C -> Res.string.language_c\n        Dart -> Res.string.language_dart\n        Ruby -> Res.string.language_ruby\n        PHP -> Res.string.language_php\n    }\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortByMapper.kt",
    "content": "package zed.rainxch.search.presentation.utils\n\nimport org.jetbrains.compose.resources.StringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.search.presentation.model.SortByUi\nimport zed.rainxch.search.presentation.model.SortByUi.*\n\nfun SortByUi.label(): StringResource =\n    when (this) {\n        MostStars -> Res.string.sort_most_stars\n        MostForks -> Res.string.sort_most_forks\n        BestMatch -> Res.string.sort_best_match\n    }\n"
  },
  {
    "path": "feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt",
    "content": "package zed.rainxch.search.presentation.utils\n\nimport org.jetbrains.compose.resources.StringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.search.presentation.model.SortOrderUi\nimport zed.rainxch.search.presentation.model.SortOrderUi.*\n\nfun SortOrderUi.label(): StringResource =\n    when (this) {\n        Descending -> Res.string.sort_order_descending\n        Ascending -> Res.string.sort_order_ascending\n    }\n"
  },
  {
    "path": "feature/starred/CLAUDE.md",
    "content": "# CLAUDE.md - Starred Feature\n\n## Purpose\n\nDisplays the user's locally saved starred repositories. This is a **presentation-only** feature with no domain or data layer -- it uses `StarredRepository` from `core/domain` directly.\n\n## Module Structure\n\n```\nfeature/starred/\n└── presentation/\n    ├── StarredReposViewModel.kt       # Observes starred repos, handles remove\n    ├── StarredReposState.kt           # starred list, loading\n    ├── StarredReposAction.kt          # RemoveStarred, click actions\n    ├── StarredReposRoot.kt            # Main composable (list of starred repos)\n    ├── model/StarredRepositoryUi.kt   # UI model for display\n    ├── mappers/StarredRepoToUiMapper.kt  # Domain → UI model mapper\n    ├── utils/TimeFormatUtils.kt       # Time formatting utilities\n    └── components/StarredRepositoryItem.kt  # Individual starred repo card\n```\n\n## Key Dependencies\n\n- `StarredRepository` (from `core/domain`) - CRUD operations for starred repos\n- Starred repos are stored locally in Room database (`StarredRepoDao` in `core/data`)\n\n## Navigation\n\nRoute: `GithubStoreGraph.StarredReposScreen` (data object, no params)\n\n## Implementation Notes\n\n- No network calls -- all data is local (Room database)\n- Uses a presentation-layer `StarredRepositoryUi` model mapped from the domain `StarredRepository` entity\n- Starring happens in other features (home, details, search); this feature only displays and removes\n- Includes its own `TimeFormatUtils` for formatting timestamps on starred items\n- The Koin module for this feature is registered in `composeApp/.../app/di/ViewModelsModule.kt` since there's no `data/di/` layer\n"
  },
  {
    "path": "feature/starred/data/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/starred/data/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n    alias(libs.plugins.convention.buildkonfig)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.feature.starred.domain)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/starred/data/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/starred/domain/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/starred/domain/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.kmp.library)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/starred/domain/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/starred/presentation/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/starred/presentation/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.convention.cmp.feature)\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n\n                implementation(projects.core.domain)\n                implementation(projects.core.presentation)\n                implementation(projects.feature.starred.domain)\n\n                implementation(libs.bundles.landscapist)\n\n                implementation(libs.kotlinx.collections.immutable)\n\n                implementation(libs.androidx.compose.ui.tooling.preview)\n                implementation(compose.components.resources)\n            }\n        }\n\n        androidMain {\n            dependencies {\n            }\n        }\n\n        jvmMain {\n            dependencies {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/starred/presentation/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposAction.kt",
    "content": "package zed.rainxch.starred.presentation\n\nimport zed.rainxch.starred.presentation.model.StarredRepositoryUi\n\nsealed interface StarredReposAction {\n    data object OnNavigateBackClick : StarredReposAction\n\n    data object OnRefresh : StarredReposAction\n\n    data object OnRetrySync : StarredReposAction\n\n    data object OnSignInClick : StarredReposAction\n\n    data object OnDismissError : StarredReposAction\n\n    data class OnRepositoryClick(\n        val repository: StarredRepositoryUi,\n    ) : StarredReposAction\n\n    data class OnDeveloperProfileClick(\n        val username: String,\n    ) : StarredReposAction\n\n    data class OnToggleFavorite(\n        val repository: StarredRepositoryUi,\n    ) : StarredReposAction\n}\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage zed.rainxch.starred.presentation\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid\nimport androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells\nimport androidx.compose.foundation.lazy.staggeredgrid.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Snackbar\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport kotlinx.collections.immutable.persistentListOf\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport zed.rainxch.core.presentation.components.GithubStoreButton\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.starred.presentation.components.StarredRepositoryItem\nimport zed.rainxch.starred.presentation.utils.formatRelativeTime\nimport kotlin.time.ExperimentalTime\n\n@Composable\nfun StarredReposRoot(\n    onNavigateBack: () -> Unit,\n    onNavigateToDetails: (repoId: Long) -> Unit,\n    onNavigateToDeveloperProfile: (username: String) -> Unit,\n    onNavigateToAuthentication: () -> Unit,\n    viewModel: StarredReposViewModel = koinViewModel(),\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n\n    StarredScreen(\n        state = state,\n        onAction = { action ->\n            when (action) {\n                StarredReposAction.OnNavigateBackClick -> onNavigateBack()\n                is StarredReposAction.OnRepositoryClick -> onNavigateToDetails(action.repository.repoId)\n                is StarredReposAction.OnDeveloperProfileClick -> onNavigateToDeveloperProfile(action.username)\n                StarredReposAction.OnSignInClick -> onNavigateToAuthentication()\n                else -> viewModel.onAction(action)\n            }\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun StarredScreen(\n    state: StarredReposState,\n    onAction: (StarredReposAction) -> Unit,\n) {\n    val pullRefreshState = rememberPullToRefreshState()\n\n    Scaffold(\n        topBar = {\n            StarredTopBar(\n                lastSyncTime = state.lastSyncTime,\n                isSyncing = state.isSyncing,\n                onAction = onAction,\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.background,\n    ) { innerPadding ->\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n        ) {\n            when {\n                !state.isAuthenticated -> {\n                    EmptyStateContent(\n                        title = stringResource(Res.string.sign_in_required),\n                        message = stringResource(Res.string.sign_in_with_github_for_stars),\n                        icon = Icons.Default.Star,\n                        actionText = stringResource(Res.string.sign_in_with_github),\n                        onActionClick = {\n                            onAction(StarredReposAction.OnSignInClick)\n                        },\n                        modifier = Modifier.align(Alignment.Center),\n                    )\n                }\n\n                state.isLoading -> {\n                    CircularWavyProgressIndicator(\n                        modifier = Modifier.align(Alignment.Center),\n                    )\n                }\n\n                state.starredRepositories.isEmpty() && !state.isSyncing -> {\n                    EmptyStateContent(\n                        title = stringResource(Res.string.no_starred_repos),\n                        message = stringResource(Res.string.star_repos_hint),\n                        icon = Icons.Default.Star,\n                        actionText = if (state.errorMessage != null) stringResource(Res.string.retry) else null,\n                        onActionClick =\n                            if (state.errorMessage != null) {\n                                {\n                                    onAction(StarredReposAction.OnRetrySync)\n                                }\n                            } else {\n                                null\n                            },\n                        modifier = Modifier.align(Alignment.Center),\n                    )\n                }\n\n                else -> {\n                    PullToRefreshBox(\n                        isRefreshing = state.isSyncing,\n                        onRefresh = {\n                            onAction(StarredReposAction.OnRefresh)\n                        },\n                        state = pullRefreshState,\n                        modifier = Modifier.fillMaxSize(),\n                    ) {\n                        LazyVerticalStaggeredGrid(\n                            columns = StaggeredGridCells.Adaptive(350.dp),\n                            verticalItemSpacing = 12.dp,\n                            horizontalArrangement = Arrangement.spacedBy(12.dp),\n                            contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp),\n                            modifier = Modifier.fillMaxSize(),\n                        ) {\n                            items(\n                                items = state.starredRepositories,\n                                key = { it.repoId },\n                            ) { repo ->\n                                StarredRepositoryItem(\n                                    repository = repo,\n                                    onToggleFavoriteClick = {\n                                        onAction(StarredReposAction.OnToggleFavorite(repo))\n                                    },\n                                    onItemClick = {\n                                        onAction(StarredReposAction.OnRepositoryClick(repo))\n                                    },\n                                    onDevProfileClick = {\n                                        onAction(StarredReposAction.OnDeveloperProfileClick(repo.repoOwner))\n                                    },\n                                    modifier = Modifier.animateItem(),\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n\n            state.errorMessage?.let { message ->\n                Snackbar(\n                    modifier =\n                        Modifier\n                            .align(Alignment.BottomCenter)\n                            .padding(16.dp),\n                    action = {\n                        TextButton(\n                            onClick = {\n                                onAction(StarredReposAction.OnRetrySync)\n                            },\n                        ) {\n                            Text(\n                                text = stringResource(Res.string.retry),\n                            )\n                        }\n                    },\n                    dismissAction = {\n                        IconButton(\n                            onClick = {\n                                onAction(StarredReposAction.OnDismissError)\n                            },\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.Close,\n                                contentDescription = stringResource(Res.string.dismiss),\n                            )\n                        }\n                    },\n                ) {\n                    Text(\n                        text = message,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun StarredTopBar(\n    lastSyncTime: Long?,\n    isSyncing: Boolean,\n    onAction: (StarredReposAction) -> Unit,\n) {\n    Column {\n        TopAppBar(\n            title = {\n                Column {\n                    Text(\n                        text = stringResource(Res.string.starred_repositories),\n                        style = MaterialTheme.typography.titleMediumEmphasized,\n                        fontWeight = FontWeight.SemiBold,\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n\n                    if (lastSyncTime != null && !isSyncing) {\n                        Text(\n                            text =\n                                \"${stringResource(Res.string.last_synced)}:\" +\n                                    \" ${formatRelativeTime(lastSyncTime)}\",\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            },\n            navigationIcon = {\n                IconButton(\n                    shapes = IconButtonDefaults.shapes(),\n                    onClick = { onAction(StarredReposAction.OnNavigateBackClick) },\n                ) {\n                    Icon(\n                        imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                        contentDescription = stringResource(Res.string.navigate_back),\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            },\n            actions = {\n                if (isSyncing) {\n                    CircularProgressIndicator(\n                        modifier =\n                            Modifier\n                                .size(24.dp)\n                                .padding(end = 12.dp),\n                        strokeWidth = 2.dp,\n                    )\n                }\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun EmptyStateContent(\n    title: String,\n    message: String,\n    icon: ImageVector,\n    modifier: Modifier = Modifier,\n    actionText: String? = null,\n    onActionClick: (() -> Unit)? = null,\n) {\n    Column(\n        modifier = modifier.padding(32.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n            modifier = Modifier.size(64.dp),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Text(\n            text = title,\n            style = MaterialTheme.typography.titleLarge,\n            color = MaterialTheme.colorScheme.onSurface,\n            textAlign = TextAlign.Center,\n        )\n\n        Spacer(modifier = Modifier.height(8.dp))\n\n        Text(\n            text = message,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n        )\n\n        if (actionText != null && onActionClick != null) {\n            Spacer(modifier = Modifier.height(16.dp))\n\n            GithubStoreButton(\n                text = actionText,\n                onClick = onActionClick,\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewStarred() {\n    GithubStoreTheme {\n        StarredScreen(\n            state =\n                StarredReposState(\n                    starredRepositories = persistentListOf(),\n                    isAuthenticated = true,\n                ),\n            onAction = {},\n        )\n    }\n}\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposState.kt",
    "content": "package zed.rainxch.starred.presentation\n\nimport kotlinx.collections.immutable.ImmutableList\nimport kotlinx.collections.immutable.persistentListOf\nimport zed.rainxch.starred.presentation.model.StarredRepositoryUi\n\ndata class StarredReposState(\n    val starredRepositories: ImmutableList<StarredRepositoryUi> = persistentListOf(),\n    val isLoading: Boolean = false,\n    val isSyncing: Boolean = false,\n    val errorMessage: String? = null,\n    val lastSyncTime: Long? = null,\n    val isAuthenticated: Boolean = false,\n)\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage zed.rainxch.starred.presentation\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.collections.immutable.toImmutableList\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.flow.onStart\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.getString\nimport zed.rainxch.core.domain.model.FavoriteRepo\nimport zed.rainxch.core.domain.repository.AuthenticationState\nimport zed.rainxch.core.domain.repository.FavouritesRepository\nimport zed.rainxch.core.domain.repository.StarredRepository\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.starred.presentation.mappers.toStarredRepositoryUi\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nclass StarredReposViewModel(\n    private val authenticationState: AuthenticationState,\n    private val starredRepository: StarredRepository,\n    private val favouritesRepository: FavouritesRepository,\n) : ViewModel() {\n    private var hasLoadedInitialData = false\n\n    private val _state = MutableStateFlow(StarredReposState())\n    val state =\n        _state\n            .onStart {\n                if (!hasLoadedInitialData) {\n                    checkAuthAndLoad()\n                    hasLoadedInitialData = true\n                }\n            }.stateIn(\n                scope = viewModelScope,\n                started = SharingStarted.WhileSubscribed(5_000L),\n                initialValue = StarredReposState(),\n            )\n\n    private fun checkAuthAndLoad() {\n        viewModelScope.launch {\n            val isAuthenticated = authenticationState.isCurrentlyUserLoggedIn()\n\n            _state.update { it.copy(isAuthenticated = isAuthenticated) }\n\n            if (isAuthenticated) {\n                loadStarredRepos()\n                syncIfNeeded()\n            }\n        }\n    }\n\n    private fun loadStarredRepos() {\n        viewModelScope.launch {\n            combine(\n                starredRepository.getAllStarred(),\n                favouritesRepository.getAllFavorites(),\n            ) { starred, favorites ->\n                val favoriteIds = favorites.map { it.repoId }.toSet()\n\n                starred.map {\n                    it.toStarredRepositoryUi(\n                        isFavorite = favoriteIds.contains(it.repoId),\n                    )\n                }\n            }.flowOn(Dispatchers.Default)\n                .collect { starredRepos ->\n                    _state.update {\n                        it.copy(\n                            starredRepositories = starredRepos.toImmutableList(),\n                            isLoading = false,\n                        )\n                    }\n                }\n        }\n    }\n\n    private fun syncIfNeeded() {\n        viewModelScope.launch {\n            if (starredRepository.needsSync()) {\n                syncStarredRepos()\n            } else {\n                val lastSync = starredRepository.getLastSyncTime()\n                _state.update { it.copy(lastSyncTime = lastSync) }\n            }\n        }\n    }\n\n    private fun syncStarredRepos(forceRefresh: Boolean = false) {\n        viewModelScope.launch {\n            _state.update { it.copy(isSyncing = true, errorMessage = null) }\n\n            val result = starredRepository.syncStarredRepos(forceRefresh)\n\n            result\n                .onSuccess {\n                    val lastSync = starredRepository.getLastSyncTime()\n                    _state.update {\n                        it.copy(\n                            isSyncing = false,\n                            lastSyncTime = lastSync,\n                        )\n                    }\n                }.onFailure { error ->\n                    _state.update {\n                        it.copy(\n                            isSyncing = false,\n                            errorMessage = error.message ?: getString(Res.string.sync_starred_failed),\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onAction(action: StarredReposAction) {\n        when (action) {\n            StarredReposAction.OnNavigateBackClick -> {\n                // Handled in composable\n            }\n\n            is StarredReposAction.OnRepositoryClick -> {\n                // Handled in composable\n            }\n\n            is StarredReposAction.OnDeveloperProfileClick -> {\n                // Handled in composable\n            }\n\n            is StarredReposAction.OnSignInClick -> {\n                // Handled in composable\n            }\n\n            StarredReposAction.OnRefresh -> {\n                syncStarredRepos(forceRefresh = true)\n            }\n\n            StarredReposAction.OnRetrySync -> {\n                syncStarredRepos(forceRefresh = true)\n            }\n\n            StarredReposAction.OnDismissError -> {\n                _state.update { it.copy(errorMessage = null) }\n            }\n\n            is StarredReposAction.OnToggleFavorite -> {\n                viewModelScope.launch {\n                    val repo = action.repository\n\n                    val favoriteRepo =\n                        FavoriteRepo(\n                            repoId = repo.repoId,\n                            repoName = repo.repoName,\n                            repoOwner = repo.repoOwner,\n                            repoOwnerAvatarUrl = repo.repoOwnerAvatarUrl,\n                            repoDescription = repo.repoDescription,\n                            primaryLanguage = repo.primaryLanguage,\n                            repoUrl = repo.repoUrl,\n                            latestVersion = repo.latestRelease,\n                            latestReleaseUrl = repo.latestReleaseUrl,\n                            addedAt = Clock.System.now().toEpochMilliseconds(),\n                            lastSyncedAt = Clock.System.now().toEpochMilliseconds(),\n                        )\n\n                    favouritesRepository.toggleFavorite(favoriteRepo)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt",
    "content": "package zed.rainxch.starred.presentation.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.CallSplit\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material.icons.outlined.FavoriteBorder\nimport androidx.compose.material.icons.outlined.Warning\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledIconToggleButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialShapes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SuggestionChip\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.toShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.skydoves.landscapist.ImageOptions\nimport com.skydoves.landscapist.coil3.CoilImage\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.core.presentation.components.ExpressiveCard\nimport zed.rainxch.core.presentation.theme.GithubStoreTheme\nimport zed.rainxch.core.presentation.utils.formatCount\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport zed.rainxch.starred.presentation.model.StarredRepositoryUi\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun StarredRepositoryItem(\n    repository: StarredRepositoryUi,\n    onToggleFavoriteClick: () -> Unit,\n    onItemClick: () -> Unit,\n    onDevProfileClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    ExpressiveCard(\n        onClick = onItemClick,\n        modifier = modifier.fillMaxWidth(),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CoilImage(\n                    imageModel = { repository.repoOwnerAvatarUrl },\n                    modifier =\n                        Modifier\n                            .size(40.dp)\n                            .clip(CircleShape)\n                            .clickable(onClick = {\n                                onDevProfileClick()\n                            }),\n                    imageOptions =\n                        ImageOptions(\n                            contentScale = ContentScale.Crop,\n                        ),\n                )\n\n                Spacer(modifier = Modifier.width(12.dp))\n\n                Column(\n                    modifier =\n                        Modifier\n                            .weight(1f)\n                            .clickable(onClick = {\n                                onDevProfileClick()\n                            }),\n                ) {\n                    Text(\n                        text = repository.repoName,\n                        style = MaterialTheme.typography.titleMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n\n                    Text(\n                        text = repository.repoOwner,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                }\n\n                FilledIconToggleButton(\n                    checked = repository.isFavorite,\n                    onCheckedChange = { onToggleFavoriteClick() },\n                    modifier = Modifier.size(40.dp),\n                    shape = MaterialShapes.Cookie6Sided.toShape(),\n                ) {\n                    Icon(\n                        imageVector =\n                            if (repository.isFavorite) {\n                                Icons.Filled.Favorite\n                            } else {\n                                Icons.Outlined.FavoriteBorder\n                            },\n                        contentDescription =\n                            if (repository.isFavorite) {\n                                stringResource(Res.string.remove_from_favourites)\n                            } else {\n                                stringResource(Res.string.add_to_favourites)\n                            },\n                        modifier = Modifier.size(20.dp),\n                    )\n                }\n            }\n\n            repository.repoDescription?.let { description ->\n                Spacer(modifier = Modifier.height(12.dp))\n\n                Text(\n                    text = description,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    maxLines = 3,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n\n            Spacer(modifier = Modifier.height(12.dp))\n\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .horizontalScroll(rememberScrollState()),\n                horizontalArrangement = Arrangement.spacedBy(16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                StatChip(\n                    icon = Icons.Default.Star,\n                    label = formatCount(repository.stargazersCount),\n                    contentDescription = \"${repository.stargazersCount} ${stringResource(Res.string.stars)}\",\n                )\n\n                StatChip(\n                    icon = Icons.AutoMirrored.Filled.CallSplit,\n                    label = formatCount(repository.forksCount),\n                    contentDescription = \"${repository.forksCount} ${stringResource(Res.string.forks)}\",\n                )\n\n                if (repository.openIssuesCount > 0) {\n                    StatChip(\n                        icon = Icons.Outlined.Warning,\n                        label = formatCount(repository.openIssuesCount),\n                        contentDescription = \"${repository.openIssuesCount} ${stringResource(Res.string.issues)}\",\n                    )\n                }\n\n                repository.primaryLanguage?.let { language ->\n                    SuggestionChip(\n                        onClick = {},\n                        label = {\n                            Text(\n                                text = language,\n                                style = MaterialTheme.typography.labelSmall,\n                            )\n                        },\n                        modifier = Modifier.height(32.dp),\n                    )\n                }\n            }\n\n            if (repository.isInstalled || repository.latestRelease != null) {\n                Spacer(modifier = Modifier.height(12.dp))\n\n                FlowRow(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    if (repository.isInstalled) {\n                        Badge(\n                            containerColor = MaterialTheme.colorScheme.primaryContainer,\n                        ) {\n                            Text(\n                                text = stringResource(Res.string.installed),\n                                style = MaterialTheme.typography.labelSmall,\n                                color = MaterialTheme.colorScheme.onPrimaryContainer,\n                            )\n                        }\n                    }\n\n                    repository.latestRelease?.let { version ->\n                        Badge(\n                            containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                        ) {\n                            Text(\n                                text = version,\n                                style = MaterialTheme.typography.labelSmall,\n                                color = MaterialTheme.colorScheme.onSecondaryContainer,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun StatChip(\n    icon: ImageVector,\n    label: String,\n    contentDescription: String,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.spacedBy(4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = contentDescription,\n            modifier = Modifier.size(16.dp),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n\n        Text(\n            text = label,\n            style = MaterialTheme.typography.labelMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewStarredRepoItem() {\n    GithubStoreTheme {\n        StarredRepositoryItem(\n            repository =\n                StarredRepositoryUi(\n                    repoId = 1,\n                    repoName = \"awesome-app\",\n                    repoOwner = \"developer\",\n                    repoOwnerAvatarUrl = \"\",\n                    repoDescription = \"An awesome application that does amazing things\",\n                    primaryLanguage = \"Kotlin\",\n                    repoUrl = \"\",\n                    stargazersCount = 1234,\n                    forksCount = 567,\n                    openIssuesCount = 12,\n                    isInstalled = true,\n                    isFavorite = false,\n                    latestRelease = \"v1.2.3\",\n                    latestReleaseUrl = null,\n                    starredAt = null,\n                ),\n            onToggleFavoriteClick = {},\n            onItemClick = {},\n            onDevProfileClick = {},\n        )\n    }\n}\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/mappers/StarredRepoToUiMapper.kt",
    "content": "package zed.rainxch.starred.presentation.mappers\n\nimport zed.rainxch.core.domain.model.StarredRepository\nimport zed.rainxch.starred.presentation.model.StarredRepositoryUi\n\nfun StarredRepository.toStarredRepositoryUi(isFavorite: Boolean = false) =\n    StarredRepositoryUi(\n        repoId = repoId,\n        repoName = repoName,\n        repoOwner = repoOwner,\n        repoOwnerAvatarUrl = repoOwnerAvatarUrl,\n        repoDescription = repoDescription,\n        primaryLanguage = primaryLanguage,\n        repoUrl = repoUrl,\n        stargazersCount = stargazersCount,\n        forksCount = forksCount,\n        openIssuesCount = openIssuesCount,\n        isInstalled = isInstalled,\n        isFavorite = isFavorite,\n        latestRelease = latestVersion,\n        latestReleaseUrl = latestReleaseUrl,\n        starredAt = starredAt,\n    )\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/model/StarredRepositoryUi.kt",
    "content": "package zed.rainxch.starred.presentation.model\n\ndata class StarredRepositoryUi(\n    val repoId: Long,\n    val repoName: String,\n    val repoOwner: String,\n    val repoOwnerAvatarUrl: String,\n    val repoDescription: String?,\n    val primaryLanguage: String?,\n    val repoUrl: String,\n    val stargazersCount: Int,\n    val forksCount: Int,\n    val openIssuesCount: Int,\n    val isInstalled: Boolean,\n    val isFavorite: Boolean = false,\n    val latestRelease: String?,\n    val latestReleaseUrl: String?,\n    val starredAt: Long?,\n)\n"
  },
  {
    "path": "feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/utils/TimeFormatUtils.kt",
    "content": "package zed.rainxch.starred.presentation.utils\n\nimport androidx.compose.runtime.Composable\nimport org.jetbrains.compose.resources.stringResource\nimport zed.rainxch.githubstore.core.presentation.res.*\nimport kotlin.time.Clock\n\n@Composable\ninternal fun formatRelativeTime(timestamp: Long): String {\n    val now = Clock.System.now().toEpochMilliseconds()\n    val diff = now - timestamp\n\n    return when {\n        diff < 60_000 -> stringResource(Res.string.just_now)\n        diff < 3600_000 -> stringResource(Res.string.minutes_ago, diff / 60_000)\n        diff < 86400_000 -> stringResource(Res.string.hours_ago, diff / 3600_000)\n        else -> stringResource(Res.string.days_ago, diff / 86400_000)\n    }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"8.13.2\"\ncomposeBom = \"2025.07.00\"\nkermit = \"2.0.8\"\nkotlin = \"2.3.10\"\nksp = \"2.3.5\"\n\nandroidDesugarJdkLibs = \"2.1.5\"\nandroidTools = \"32.0.1\"\n\n# Compose\ncompose-hot-reload = \"1.0.0\"\ncompose-multiplatform = \"1.10.1\"\ncompose-lifecycle = \"2.9.6\"\nnavigation-compose = \"2.9.2\"\njetbrains-core-bundle = \"1.0.1\"\nmaterial-icons = \"1.7.3\"\n\n# AndroidX\nandroidx-activity = \"1.12.4\"\ncore-splashscreen = \"1.2.0\"\n\n# Kotlinx\nkotlinx-coroutines = \"1.10.2\"\nkotlinx-serialization = \"1.10.0\"\nkotlinx-datetime = \"0.7.1\"\nkotlinx-collections-immutable = \"0.4.0\"\n\n# Third party\nkoin = \"4.1.1\"\nktor = \"3.4.0\"\nroom = \"2.8.4\"\nslf4jSimple = \"2.0.17\"\nsqlite = \"2.6.2\"\ndatastore = \"1.2.0\"\ncompose-jetbrains = \"1.10.1\"\ncompose-jetbrains-material-you = \"1.11.0-alpha03\"\njetbrains-savedstate = \"1.4.0\"\nmoko = \"0.20.1\"\nbuildkonfig = \"0.17.1\"\nmarkdownRenderer = \"0.39.2\"\nliquid = \"1.1.1\"\nlandscapist = \"2.9.5\"\nshizuku = \"13.1.5\"\nhidden-api = \"4.4.0\"\njsystemthemedetector = \"3.9.1\"\nwork = \"2.11.1\"\nresources=\"1.10.1\"\n\nktlint-gradle = \"12.1.1\"\n\nprojectApplicationId = \"zed.rainxch.githubstore\"\nprojectVersionName = \"1.6.2\"\nprojectMinSdkVersion = \"26\"\nprojectTargetSdkVersion = \"36\"\nprojectCompileSdkVersion = \"36\"\nprojectVersionCode = \"13\"\n\n[libraries]\n# Kotlin\nandroidx-compose-bom = { module = \"androidx.compose:compose-bom\", version.ref = \"composeBom\" }\nkotlin-stdlib = { module = \"org.jetbrains.kotlin:kotlin-stdlib\", version.ref = \"kotlin\" }\n\n# AndroidX Core\nandroidx-activity-compose = { module = \"androidx.activity:activity-compose\", version.ref = \"androidx-activity\" }\njetbrains-compose-viewmodel = { module = \"org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose\", version.ref = \"compose-lifecycle\" }\nkoin-core-viewmodel = { group = \"io.insert-koin\", name = \"koin-core-viewmodel\", version.ref = \"koin\" }\ncore-splashscreen = { group = \"androidx.core\", name = \"core-splashscreen\", version.ref = \"core-splashscreen\" }\n\n# Kotlinx\nkotlinx-coroutines-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinx-coroutines\" }\nkotlinx-coroutines-swing = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-swing\", version.ref = \"kotlinx-coroutines\" }\nkotlinx-serialization-json = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json\", version.ref = \"kotlinx-serialization\" }\nkotlinx-datetime = { module = \"org.jetbrains.kotlinx:kotlinx-datetime\", version.ref = \"kotlinx-datetime\" }\nkotlinx-collections-immutable = { module = \"org.jetbrains.kotlinx:kotlinx-collections-immutable\", version.ref = \"kotlinx-collections-immutable\" }\n\n# Compose\njetbrains-compose-navigation = { module = \"org.jetbrains.androidx.navigation:navigation-compose\", version.ref = \"navigation-compose\" }\n\n# Koin\nkoin-core = { module = \"io.insert-koin:koin-core\", version.ref = \"koin\" }\nkoin-android = { module = \"io.insert-koin:koin-android\", version.ref = \"koin\" }\nkoin-compose = { module = \"io.insert-koin:koin-compose\", version.ref = \"koin\" }\nkoin-compose-viewmodel = { module = \"io.insert-koin:koin-compose-viewmodel\", version.ref = \"koin\" }\nkoin-androidx-compose = { group = \"io.insert-koin\", name = \"koin-androidx-compose\", version.ref = \"koin\" }\nkoin-androidx-navigation = { group = \"io.insert-koin\", name = \"koin-androidx-navigation\", version.ref = \"koin\" }\n\njsystemthemedetector = { module = \"com.github.Dansoftowner:jSystemThemeDetector\", version.ref = \"jsystemthemedetector\" }\n\n# Ktor\nktor-client-core = { module = \"io.ktor:ktor-client-core\", version.ref = \"ktor\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-cio = { module = \"io.ktor:ktor-client-cio\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-client-logging = { module = \"io.ktor:ktor-client-logging\", version.ref = \"ktor\" }\nktor-client-auth = { module = \"io.ktor:ktor-client-auth\", version.ref = \"ktor\" }\nktor-serialization-kotlinx-json = { module = \"io.ktor:ktor-serialization-kotlinx-json\", version.ref = \"ktor\" }\n\n# DataStore\ndatastore = { module = \"androidx.datastore:datastore\", version.ref = \"datastore\" }\ndatastore-preferences = { module = \"androidx.datastore:datastore-preferences\", version.ref = \"datastore\" }\n\n# Room\nandroidx-room-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"room\" }\nandroidx-room-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"room\" }\nslf4j-simple = { module = \"org.slf4j:slf4j-simple\", version.ref = \"slf4jSimple\" }\nsqlite-bundled = { module = \"androidx.sqlite:sqlite-bundled\", version.ref = \"sqlite\" }\n\n# Image loading\nlandscapist-image = { module = \"com.github.skydoves:landscapist-coil3\", version.ref = \"landscapist\" }\nlandscapist-core = { module = \"com.github.skydoves:landscapist-core\", version.ref = \"landscapist\" }\n\n#Permission handling\nmoko-permissions = { module = \"dev.icerock.moko:permissions\", version.ref = \"moko\" }\nmoko-permissions-compose = { module = \"dev.icerock.moko:permissions-compose\", version.ref = \"moko\" }\nmoko-permissions-notifications = { module = \"dev.icerock.moko:permissions-notifications\", version.ref = \"moko\" }\n\nandroid-desugarJdkLibs = { module = \"com.android.tools:desugar_jdk_libs\", version.ref = \"androidDesugarJdkLibs\" }\nandroid-gradlePlugin = { group = \"com.android.tools.build\", name = \"gradle\", version.ref = \"agp\" }\nandroid-tools-common = { group = \"com.android.tools\", name = \"common\", version.ref = \"androidTools\" }\ncompose-gradlePlugin = { group = \"org.jetbrains.kotlin\", name = \"compose-compiler-gradle-plugin\", version.ref = \"kotlin\" }\nkotlin-gradlePlugin = { module = \"org.jetbrains.kotlin:kotlin-gradle-plugin\", version.ref = \"kotlin\" }\nandroidx-room-gradle-plugin = { module = \"androidx.room:room-gradle-plugin\", version.ref = \"room\" }\n\nbuildkonfig-gradlePlugin = { group = \"com.codingfeline.buildkonfig\", name = \"buildkonfig-gradle-plugin\", version.ref = \"buildkonfig\" }\nbuildkonfig-compiler = { group = \"com.codingfeline.buildkonfig\", name = \"buildkonfig-compiler\", version.ref = \"buildkonfig\" }\n\ntouchlab-kermit = { module = \"co.touchlab:kermit\", version.ref = \"kermit\" }\n\nandroidx-compose-ui-tooling-preview = { group = \"org.jetbrains.compose.ui\", name = \"ui-tooling-preview\", version.ref=\"resources\" }\nandroidx-compose-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\n\nkoin-bom = { group = \"io.insert-koin\", name = \"koin-bom\", version.ref = \"koin\" }\njetbrains-compose-runtime = { module = \"org.jetbrains.compose.runtime:runtime\", version.ref = \"compose-jetbrains\" }\njetbrains-compose-material3 = { module = \"org.jetbrains.compose.material3:material3\", version.ref = \"compose-jetbrains-material-you\" }\njetbrains-compose-material-icons-core = { module = \"org.jetbrains.compose.material:material-icons-core\", version.ref = \"material-icons\" }\njetbrains-compose-material-icons-extended = { module = \"org.jetbrains.compose.material:material-icons-extended\", version.ref = \"material-icons\" }\njetbrains-compose-ui = { module = \"org.jetbrains.compose.ui:ui\", version.ref = \"compose-jetbrains\" }\njetbrains-compose-foundation = { module = \"org.jetbrains.compose.foundation:foundation\", version.ref = \"compose-jetbrains\" }\n\njetbrains-lifecycle-viewmodel = { module = \"org.jetbrains.androidx.lifecycle:lifecycle-viewmodel\", version.ref = \"compose-lifecycle\" }\njetbrains-lifecycle-compose = { group = \"org.jetbrains.androidx.lifecycle\", name = \"lifecycle-runtime-compose\", version.ref = \"compose-lifecycle\" }\njetbrains-lifecycle-viewmodel-savedstate = { module = \"org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate\", version.ref = \"compose-lifecycle\" }\njetbrains-savedstate = { module = \"org.jetbrains.androidx.savedstate:savedstate\", version.ref = \"jetbrains-savedstate\" }\njetbrains-bundle = { module = \"org.jetbrains.androidx.core:core-bundle\", version.ref = \"jetbrains-core-bundle\" }\n\n# Markdown\nmarkdown-renderer = { module = \"com.mikepenz:multiplatform-markdown-renderer-m3\", version.ref = \"markdownRenderer\" }\nmarkdown-renderer-coil3 = { module = \"com.mikepenz:multiplatform-markdown-renderer-coil3\", version.ref = \"markdownRenderer\" }\n\n# WorkManager\nandroidx-work-runtime = { module = \"androidx.work:work-runtime-ktx\", version.ref = \"work\" }\n\n# Shizuku\nshizuku-api = { module = \"dev.rikka.shizuku:api\", version.ref = \"shizuku\" }\nshizuku-provider = { module = \"dev.rikka.shizuku:provider\", version.ref = \"shizuku\" }\nhidden-api-stub = { module = \"dev.rikka.hidden:stub\", version.ref = \"hidden-api\" }\n\n# Liquid effect\nliquid = { module = \"io.github.fletchmckee.liquid:liquid\", version.ref = \"liquid\" }\n\nktlint-gradlePlugin = { group = \"org.jlleitschuh.gradle\", name=\"ktlint-gradle\", version.ref = \"ktlint-gradle\" }\n\n[plugins]\n\nconvention-cmp-application = { id = \"zed.rainxch.convention.cmp.application\", version = \"unspecified\" }\nconvention-kmp-library = { id = \"zed.rainxch.convention.kmp.library\", version = \"unspecified\" }\nconvention-cmp-library = { id = \"zed.rainxch.convention.cmp.library\", version = \"unspecified\" }\nconvention-cmp-feature = { id = \"zed.rainxch.convention.cmp.feature\", version = \"unspecified\" }\nconvention-room = { id = \"zed.rainxch.convention.room\", version = \"unspecified\" }\nconvention-buildkonfig = { id = \"zed.rainxch.convention.buildkonfig\", version=\"unspecified\" }\n\n\n# Android\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nandroid-library = { id = \"com.android.library\", version.ref = \"agp\" }\nandroid-kotlin-multiplatform-library = { id = \"com.android.kotlin.multiplatform.library\", version.ref = \"agp\" }\n\n# Kotlin\nkotlin-multiplatform = { id = \"org.jetbrains.kotlin.multiplatform\", version.ref = \"kotlin\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\n\n# Compose\ncompose-hot-reload = { id = \"org.jetbrains.compose.hot-reload\", version.ref = \"compose-hot-reload\" }\ncompose-multiplatform = { id = \"org.jetbrains.compose\", version.ref = \"compose-multiplatform\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\n\n# Build tools\nksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nroom = { id = \"androidx.room\", version.ref = \"room\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nbuildkonfig = { id = \"com.codingfeline.buildkonfig\", version.ref = \"buildkonfig\" }\n\n[bundles]\nkoin-common = [\n    \"koin-core\",\n    \"koin-compose\",\n    \"koin-compose-viewmodel\"\n]\n\nktor-common = [\n    \"ktor-client-core\",\n    \"ktor-client-content-negotiation\",\n    \"ktor-serialization-kotlinx-json\",\n    \"ktor-client-auth\",\n    \"ktor-client-logging\"\n]\n\nlandscapist = [\n    \"landscapist-core\",\n    \"landscapist-image\"\n]"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14.3-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "#Kotlin\nkotlin.code.style=official\nkotlin.daemon.jvmargs=-Xmx3072M\n\n#Gradle\norg.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8\norg.gradle.configuration-cache=true\norg.gradle.caching=true\norg.gradle.parallel=true\n\n#Android\nandroid.nonTransitiveRClass=true\nandroid.useAndroidX=true\n\ncompose.desktop.packaging.checkJdkVendor=false"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=\"\\\\\\\"\\\\\\\"\"\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n@rem SPDX-License-Identifier: Apache-2.0\n@rem\n\n@if \"%DEBUG%\"==\"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\n@rem This is normally unused\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif %ERRORLEVEL% equ 0 goto execute\n\necho. 1>&2\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\necho. 1>&2\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\necho location of your Java installation. 1>&2\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho. 1>&2\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\necho. 1>&2\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\necho location of your Java installation. 1>&2\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif %ERRORLEVEL% equ 0 goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nset EXIT_CODE=%ERRORLEVEL%\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\nexit /b %EXIT_CODE%\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "packaging/flatpak/README.md",
    "content": "# Flatpak Packaging for GitHub Store\n\n## Prerequisites\n\nInstall Flatpak and the build tools:\n\n```bash\n# Fedora\nsudo dnf install flatpak flatpak-builder\n\n# Ubuntu/Debian\nsudo apt install flatpak flatpak-builder\n\n# Arch\nsudo pacman -S flatpak flatpak-builder\n```\n\nInstall the required runtimes:\n\n```bash\nflatpak install flathub org.freedesktop.Platform//24.08\nflatpak install flathub org.freedesktop.Sdk//24.08\nflatpak install flathub org.freedesktop.Sdk.Extension.openjdk21//24.08\n```\n\n## Setup (One-Time)\n\n### 1. Generate Gradle dependency sources\n\nFlatpak builds run without network access, so all Maven/Gradle dependencies must\nbe pre-downloaded and listed in a JSON manifest.\n\nAdd the plugin to your root `build.gradle.kts`:\n\n```kotlin\nplugins {\n    id(\"io.github.jwharm.flatpak-gradle-generator\") version \"1.7.0\"\n}\n```\n\nThen generate the sources file:\n\n```bash\n./gradlew flatpakGradleGenerator --no-configuration-cache\n```\n\nThis creates `flatpak-sources.json` in the project root. Move it to this directory:\n\n```bash\nmv flatpak-sources.json packaging/flatpak/\n```\n\n### 2. Verify SHA256 hashes\n\nThe manifest uses pre-computed SHA256 hashes. To verify or update them:\n\n```bash\n# Gradle distribution\ncurl -sL https://services.gradle.org/distributions/gradle-8.14.3-bin.zip | sha256sum\n\n# JBR x64 (check latest at https://github.com/JetBrains/JetBrainsRuntime/releases)\ncurl -sL https://cache-redirector.jetbrains.com/intellij-jbr/jbr-21.0.10-linux-x64-b1163.105.tar.gz | sha256sum\n\n# JBR aarch64\ncurl -sL https://cache-redirector.jetbrains.com/intellij-jbr/jbr-21.0.10-linux-aarch64-b1163.105.tar.gz | sha256sum\n```\n\n### 3. Update screenshot URLs\n\nEdit `zed.rainxch.githubstore.metainfo.xml` to point to hosted screenshot images.\nFlathub requires at least one screenshot with a publicly accessible URL.\n\n## Building Locally\n\n```bash\ncd packaging/flatpak\n\n# Build\nflatpak-builder --force-clean build-dir zed.rainxch.githubstore.yml\n\n# Test run\nflatpak-builder --run build-dir zed.rainxch.githubstore.yml githubstore\n\n# Install locally\nflatpak-builder --user --install --force-clean build-dir zed.rainxch.githubstore.yml\n```\n\n## Validating\n\n```bash\n# Validate AppStream metainfo\nflatpak run org.freedesktop.appstream-glib validate zed.rainxch.githubstore.metainfo.xml\n\n# Lint manifest (requires org.flatpak.Builder)\nflatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest zed.rainxch.githubstore.yml\n```\n\n## Publishing to Flathub\n\n1. Fork `https://github.com/flathub/flathub`\n2. Checkout the `new-pr` branch\n3. Copy the manifest YAML and `flatpak-sources.json` to the repo root\n4. Open a PR titled \"Add zed.rainxch.githubstore\"\n5. Reviewers will trigger test builds with `bot, build`\n6. After approval, you get write access to `flathub/zed.rainxch.githubstore`\n\n## File Reference\n\n| File | Purpose |\n|------|---------|\n| `zed.rainxch.githubstore.yml` | Flatpak build manifest |\n| `zed.rainxch.githubstore.desktop` | Desktop launcher entry |\n| `zed.rainxch.githubstore.metainfo.xml` | AppStream metadata for Flathub listing |\n| `githubstore.sh` | Shell launcher (invokes `java -jar` with bundled JRE) |\n| `disable-android-for-flatpak.sh` | Strips Android targets for sandbox build |\n| `flatpak-sources.json` | Pre-downloaded Gradle dependencies (generated) |\n"
  },
  {
    "path": "packaging/flatpak/disable-android-for-flatpak.sh",
    "content": "#!/bin/bash\n# disable-android-for-flatpak.sh\n#\n# Strips all Android-related configuration from the project so it can\n# build inside the Flatpak sandbox where no Android SDK is available.\n# This script modifies files IN-PLACE — only run during Flatpak builds.\n\nset -euo pipefail\n\necho \"=== Disabling Android targets for Flatpak build ===\"\n\n# ─────────────────────────────────────────────────────────────────────\n# 1. Root build.gradle.kts — comment out Android plugin declarations\n# ─────────────────────────────────────────────────────────────────────\necho \"[1/6] Patching root build.gradle.kts\"\nsed -i \\\n    -e 's|alias(libs.plugins.android.application)|// alias(libs.plugins.android.application)|' \\\n    -e 's|alias(libs.plugins.android.library)|// alias(libs.plugins.android.library)|' \\\n    -e 's|alias(libs.plugins.android.kotlin.multiplatform.library)|// alias(libs.plugins.android.kotlin.multiplatform.library)|' \\\n    build.gradle.kts\n\n# ─────────────────────────────────────────────────────────────────────\n# 2. Convention plugins — replace Android plugin applies with no-ops\n# ─────────────────────────────────────────────────────────────────────\nCONVENTION_DIR=\"build-logic/convention/src/main/kotlin\"\n\necho \"[2/6] Patching KmpLibraryConventionPlugin (remove Android library plugin + config)\"\ncat > \"$CONVENTION_DIR/KmpLibraryConventionPlugin.kt\" << 'KOTLIN'\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.configureJvmTarget\nimport zed.rainxch.githubstore.convention.libs\nimport zed.rainxch.githubstore.convention.pathToResourcePrefix\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\nimport org.gradle.kotlin.dsl.configure\n\nclass KmpLibraryConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"org.jetbrains.kotlin.multiplatform\")\n                apply(\"org.jetbrains.kotlin.plugin.serialization\")\n            }\n\n            configureJvmTarget()\n\n            extensions.configure<KotlinMultiplatformExtension> {\n                compilerOptions {\n                    freeCompilerArgs.add(\"-Xexpect-actual-classes\")\n                    freeCompilerArgs.add(\"-Xmulti-dollar-interpolation\")\n                    freeCompilerArgs.add(\"-opt-in=kotlin.RequiresOptIn\")\n                    freeCompilerArgs.add(\"-opt-in=kotlin.time.ExperimentalTime\")\n                }\n            }\n\n            dependencies {\n                \"commonMainImplementation\"(libs.findLibrary(\"kotlinx-serialization-json\").get())\n            }\n        }\n    }\n}\nKOTLIN\n\necho \"[3/6] Patching CmpApplicationConventionPlugin (remove Android application)\"\ncat > \"$CONVENTION_DIR/CmpApplicationConventionPlugin.kt\" << 'KOTLIN'\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.dependencies\nimport zed.rainxch.githubstore.convention.configureJvmTarget\nimport zed.rainxch.githubstore.convention.libs\n\nclass CmpApplicationConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"org.jetbrains.kotlin.multiplatform\")\n                apply(\"org.jetbrains.compose\")\n                apply(\"org.jetbrains.kotlin.plugin.compose\")\n            }\n\n            configureJvmTarget()\n        }\n    }\n}\nKOTLIN\n\necho \"[4/6] Patching CmpLibraryConventionPlugin & CmpFeatureConventionPlugin\"\n\n# CmpLibraryConventionPlugin — remove Android library dependency\nsed -i \\\n    -e 's|apply(\"com.android.library\")|// apply(\"com.android.library\")|' \\\n    \"$CONVENTION_DIR/CmpLibraryConventionPlugin.kt\" 2>/dev/null || true\n\nsed -i \\\n    -e 's|apply(\"com.android.library\")|// apply(\"com.android.library\")|' \\\n    \"$CONVENTION_DIR/CmpFeatureConventionPlugin.kt\" 2>/dev/null || true\n\n# Remove configureKotlinAndroid calls and Android extension blocks (only at call sites)\nfor f in \"$CONVENTION_DIR\"/*.kt \"$CONVENTION_DIR\"/zed/rainxch/githubstore/convention/*.kt; do\n    [ -f \"$f\" ] || continue\n    # Skip files that define these functions (declarations, not call sites)\n    grep -q \"fun Project.configureAndroidTarget\" \"$f\" && continue\n    grep -q \"fun Project.configureKotlinAndroid\" \"$f\" && continue\n    sed -i \\\n        -e 's|configureAndroidTarget()|// configureAndroidTarget()|g' \\\n        -e 's|configureKotlinAndroid(this)|// configureKotlinAndroid(this)|g' \\\n        \"$f\"\ndone\n\n# ─────────────────────────────────────────────────────────────────────\n# 3. KotlinMultiplatform.kt — skip Android configuration\n# ─────────────────────────────────────────────────────────────────────\necho \"[5/6] Patching KotlinMultiplatform.kt\"\ncat > \"$CONVENTION_DIR/zed/rainxch/githubstore/convention/KotlinMultiplatform.kt\" << 'KOTLIN'\npackage zed.rainxch.githubstore.convention\n\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\n\ninternal fun Project.configureKotlinMultiplatform() {\n    // Android target disabled for Flatpak build\n    configureJvmTarget()\n\n    extensions.configure<KotlinMultiplatformExtension> {\n        compilerOptions {\n            freeCompilerArgs.add(\"-Xexpect-actual-classes\")\n            freeCompilerArgs.add(\"-Xmulti-dollar-interpolation\")\n            freeCompilerArgs.add(\"-opt-in=kotlin.RequiresOptIn\")\n            freeCompilerArgs.add(\"-opt-in=kotlin.time.ExperimentalTime\")\n        }\n    }\n}\nKOTLIN\n\n# ─────────────────────────────────────────────────────────────────────\n# 4. Module build.gradle.kts files — remove android {} blocks\n# ─────────────────────────────────────────────────────────────────────\necho \"[6/6] Removing android {} blocks from module build.gradle.kts files\"\n\n# composeApp — remove android {} block and its contents\npython3 -c \"\nimport re, sys\n\nwith open('composeApp/build.gradle.kts', 'r') as f:\n    content = f.read()\n\n# Remove top-level android { ... } blocks (handles nested braces)\ndef remove_block(text, keyword):\n    result = []\n    i = 0\n    while i < len(text):\n        # Look for 'android {' at line start (possibly with whitespace)\n        line_start = text.rfind('\\n', 0, i) + 1\n        prefix = text[line_start:i].strip()\n        if text[i:].startswith(keyword + ' {') or text[i:].startswith(keyword + '{'):\n            if prefix == '' or prefix.endswith('\\n'):\n                # Find matching closing brace\n                brace_start = text.index('{', i)\n                depth = 1\n                j = brace_start + 1\n                while j < len(text) and depth > 0:\n                    if text[j] == '{': depth += 1\n                    elif text[j] == '}': depth -= 1\n                    j += 1\n                # Skip past the block and any trailing newline\n                if j < len(text) and text[j] == '\\n':\n                    j += 1\n                i = j\n                continue\n        result.append(text[i])\n        i += 1\n    return ''.join(result)\n\ncontent = remove_block(content, 'android')\nwith open('composeApp/build.gradle.kts', 'w') as f:\n    f.write(content)\n\"\n\n# core/data — remove android {} block\nfor gradle_file in \\\n    core/data/build.gradle.kts \\\n    core/domain/build.gradle.kts \\\n    core/presentation/build.gradle.kts; do\n    if [ -f \"$gradle_file\" ]; then\n        python3 -c \"\nimport sys\nwith open('$gradle_file', 'r') as f:\n    lines = f.readlines()\nresult = []\nskip_depth = 0\ni = 0\nwhile i < len(lines):\n    stripped = lines[i].strip()\n    if stripped.startswith('android {') or stripped == 'android{':\n        skip_depth = 1\n        i += 1\n        while i < len(lines) and skip_depth > 0:\n            for ch in lines[i]:\n                if ch == '{': skip_depth += 1\n                elif ch == '}': skip_depth -= 1\n            i += 1\n        continue\n    result.append(lines[i])\n    i += 1\nwith open('$gradle_file', 'w') as f:\n    f.writelines(result)\n\"\n    fi\ndone\n\n# Remove AndroidApplicationComposeConventionPlugin registration attempt\n# (it won't compile without AGP)\ncat > \"$CONVENTION_DIR/AndroidApplicationComposeConventionPlugin.kt\" << 'KOTLIN'\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\n\nclass AndroidApplicationComposeConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        // No-op: Android disabled for Flatpak build\n    }\n}\nKOTLIN\n\ncat > \"$CONVENTION_DIR/AndroidApplicationConventionPlugin.kt\" << 'KOTLIN'\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\n\nclass AndroidApplicationConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        // No-op: Android disabled for Flatpak build\n    }\n}\nKOTLIN\n\necho \"=== Android targets disabled successfully ===\"\n"
  },
  {
    "path": "packaging/flatpak/flatpak-sources.json",
    "content": "[\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/8.13.2/databinding-common-8.13.2.jar\",\n    \"sha512\": \"5d7539ba242138ae5ed2f0ddc9da6b0c3700bdf67c763196dc26e3557172f1a97284a3bb9592d3049bf96cab89602d0e65df95c7e3cb73f7c4632b166bae6ebd\",\n    \"dest\": \"offline-repository/androidx/databinding/databinding-common/8.13.2\",\n    \"dest-filename\": \"databinding-common-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-common/8.13.2/databinding-common-8.13.2.pom\",\n    \"sha512\": \"620d33b4a75059ecea751d5fa54c94122beff6bf00df1afea0a887a22346cc59f511e58275a16f5066741a11950404686ad1d093ecf22e3304899a8bab6b16cd\",\n    \"dest\": \"offline-repository/androidx/databinding/databinding-common/8.13.2\",\n    \"dest-filename\": \"databinding-common-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/8.13.2/databinding-compiler-common-8.13.2.jar\",\n    \"sha512\": \"652d1833694c9311982fed2c5b41c6004c72a4051c75cedd2ec3acd2f968c7994d5fcf5e56d2d8013944948e95fa2f684c9a6cac4c4c79977a2d1c335365b729\",\n    \"dest\": \"offline-repository/androidx/databinding/databinding-compiler-common/8.13.2\",\n    \"dest-filename\": \"databinding-compiler-common-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/databinding/databinding-compiler-common/8.13.2/databinding-compiler-common-8.13.2.pom\",\n    \"sha512\": \"3c71b98612f8edb0dccc5074bc09b80d67b4eb4ba8ebde5e816cbbcdd0deee102c9d32e0921e74bffdc2106faeb1f4b0261acf56a82bc55ffd765b9cd39c9aab\",\n    \"dest\": \"offline-repository/androidx/databinding/databinding-compiler-common/8.13.2\",\n    \"dest-filename\": \"databinding-compiler-common-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/androidx.room.gradle.plugin/2.8.4/androidx.room.gradle.plugin-2.8.4.pom\",\n    \"sha512\": \"948c7fbe9ecdb9f1f2a88a158411295be44ce60ffc64225c6842a0a08a938c50193e205dac0f60e8d66e0c4a940b37196c66cd4b2229870dbd215b56a27d85b4\",\n    \"dest\": \"offline-repository/androidx/room/androidx.room.gradle.plugin/2.8.4\",\n    \"dest-filename\": \"androidx.room.gradle.plugin-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-common/2.8.4/room-common-2.8.4.pom\",\n    \"sha512\": \"50105d8200b9af90a687bb2c9f5e532925ac89b6be0541463c2c6116accf6dc265a310f408697833d0e5020d6402b363fc224e6a571f551416d1cd4aa8ea12c8\",\n    \"dest\": \"offline-repository/androidx/room/room-common/2.8.4\",\n    \"dest-filename\": \"room-common-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-compiler-processing-testing/2.8.4/room-compiler-processing-testing-2.8.4.pom\",\n    \"sha512\": \"388f70063e366cf25ef58d7401947ac7f1c6801e29f9b9289dbec696b11177ee239cec49bc8c9f6733145e96856669e6c42655888b1ddd48bead60a870af63e7\",\n    \"dest\": \"offline-repository/androidx/room/room-compiler-processing-testing/2.8.4\",\n    \"dest-filename\": \"room-compiler-processing-testing-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-compiler-processing/2.8.4/room-compiler-processing-2.8.4.pom\",\n    \"sha512\": \"9e88ff50c938724b5f29b18c1914e5b9a83d29b8e96c03beea2daa86a4b81aa665f30ffbe8d79bef7db9a65922c84d4b76c38fc99fad8ed38d13b8306e21dc30\",\n    \"dest\": \"offline-repository/androidx/room/room-compiler-processing/2.8.4\",\n    \"dest-filename\": \"room-compiler-processing-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-compiler/2.8.4/room-compiler-2.8.4.pom\",\n    \"sha512\": \"5f856c59574a0519cc65eec1053a08e85aac6f08fa3dbfad51ca2c697d47e005681e03c51f3643f60b31c3ee77802c787bae404f552ee9ef0ae811441aeb40e7\",\n    \"dest\": \"offline-repository/androidx/room/room-compiler/2.8.4\",\n    \"dest-filename\": \"room-compiler-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-external-antlr/2.8.4/room-external-antlr-2.8.4.pom\",\n    \"sha512\": \"cd524e47bfcfc66c1a6f18e28d67e84b643102f950d8b9c5903b5c0d71b9a13359e8bf71412bc9b86d78e576421ec8714631e108249be7f45b5324ac1ff579e8\",\n    \"dest\": \"offline-repository/androidx/room/room-external-antlr/2.8.4\",\n    \"dest-filename\": \"room-external-antlr-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-gradle-plugin/2.8.4/room-gradle-plugin-2.8.4.jar\",\n    \"sha512\": \"44d219f383e862c2f052a7f076d41a3ca6fd87274d6866dc5ae59cbb805cc43b048a978723d263ef7bd48b55f72235d96e3a19ebd52b58bb800c2e8e751df43c\",\n    \"dest\": \"offline-repository/androidx/room/room-gradle-plugin/2.8.4\",\n    \"dest-filename\": \"room-gradle-plugin-2.8.4.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-gradle-plugin/2.8.4/room-gradle-plugin-2.8.4.module\",\n    \"sha512\": \"c913ef5e3a35cfe1a1d7324bf43c0f1afba416f1743968f745a5d2115752df7abe0a886de50541b44de8b6447f07b5ab460a8198a3872a237ad20e9a5821fd07\",\n    \"dest\": \"offline-repository/androidx/room/room-gradle-plugin/2.8.4\",\n    \"dest-filename\": \"room-gradle-plugin-2.8.4.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-gradle-plugin/2.8.4/room-gradle-plugin-2.8.4.pom\",\n    \"sha512\": \"793b35974dbfeeb8bc452acfeba4c13d9c84ce2155f23e0cffd4cc7cd724d179c7a2728aeeacc6d6016f94a408a4e074022bef96ae2c0688272e1b2df83e8773\",\n    \"dest\": \"offline-repository/androidx/room/room-gradle-plugin/2.8.4\",\n    \"dest-filename\": \"room-gradle-plugin-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-guava/2.8.4/room-guava-2.8.4.pom\",\n    \"sha512\": \"6f148fd3ed526c4e1a4651ef31349db9279ddcf80162b6a26c1f3cb5b23604f63a7f2e547f51752f8e503764b5458cd7b2e7a658035475a84c24d1578407c4d9\",\n    \"dest\": \"offline-repository/androidx/room/room-guava/2.8.4\",\n    \"dest-filename\": \"room-guava-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-ktx/2.8.4/room-ktx-2.8.4.pom\",\n    \"sha512\": \"1952df8cd6f341d980b2dbb8cfad9db7e303fcc153f11a9c998382944b9ff9430f0c19a55b3a4c9cf8d119242eede0b99f0d6481506541e0bc708e58b8a203ba\",\n    \"dest\": \"offline-repository/androidx/room/room-ktx/2.8.4\",\n    \"dest-filename\": \"room-ktx-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-migration/2.8.4/room-migration-2.8.4.pom\",\n    \"sha512\": \"f5a5d6f387ca02767742ccc1b77a7677b3d9ab159fd56a71250df9fafafb7aa98bb3dab8f4af0b8648b983f7e54bce9e1966184e91be80af0ec47fe3f268982e\",\n    \"dest\": \"offline-repository/androidx/room/room-migration/2.8.4\",\n    \"dest-filename\": \"room-migration-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-paging-guava/2.8.4/room-paging-guava-2.8.4.pom\",\n    \"sha512\": \"b7456558c9e275de0c99d4c08cf07834bcca3df4e89d245d1b8e9b3dca4a245fa0e6d3c5e88e4a484e5d817fd07abda2641e9b47871e22a6c52953c42624ac1b\",\n    \"dest\": \"offline-repository/androidx/room/room-paging-guava/2.8.4\",\n    \"dest-filename\": \"room-paging-guava-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-paging-rxjava2/2.8.4/room-paging-rxjava2-2.8.4.pom\",\n    \"sha512\": \"2f2e0ab85e10d99b83c14e4cdd83e750cb86927fb59566d3b63795d0ee24e7b54d3fe57772e3394f9f61403622f421faec22fe0ac5ad4271d177024a16aad093\",\n    \"dest\": \"offline-repository/androidx/room/room-paging-rxjava2/2.8.4\",\n    \"dest-filename\": \"room-paging-rxjava2-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-paging-rxjava3/2.8.4/room-paging-rxjava3-2.8.4.pom\",\n    \"sha512\": \"985d79c62eee22c7f3698d570d9d431fef054e0b8260026fde6e66c2f3216c83e37182e2f66b7e994324a1917931e6727aca331688e49790513e725256cef89a\",\n    \"dest\": \"offline-repository/androidx/room/room-paging-rxjava3/2.8.4\",\n    \"dest-filename\": \"room-paging-rxjava3-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-paging/2.8.4/room-paging-2.8.4.pom\",\n    \"sha512\": \"b317fc765b34d37502cbdf6aebab75502d9fac73b904773cea5762afbc69772c4eeef38b4b3f7386ea92f403b3cedff000c69ab74dcc3c85d5179a3e31ea6321\",\n    \"dest\": \"offline-repository/androidx/room/room-paging/2.8.4\",\n    \"dest-filename\": \"room-paging-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-runtime/2.8.4/room-runtime-2.8.4.pom\",\n    \"sha512\": \"550b98396d2d83da40ee9147fe6fd41bbfe06a1c115fbda7a794a8c2c3434b527016e8cd83ecc80eddc5bbc1444c20e584200bd9b19a7a430c440674adf42c79\",\n    \"dest\": \"offline-repository/androidx/room/room-runtime/2.8.4\",\n    \"dest-filename\": \"room-runtime-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-rxjava2/2.8.4/room-rxjava2-2.8.4.pom\",\n    \"sha512\": \"57f771cf8611699327ae3a680adf3dbf0c7cf03e3288a04e2e1a969b7ad8e443ef020d52c7e6fd125d98311bd93bf6fbaa7856a2175698a599fc748a73b8b186\",\n    \"dest\": \"offline-repository/androidx/room/room-rxjava2/2.8.4\",\n    \"dest-filename\": \"room-rxjava2-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-rxjava3/2.8.4/room-rxjava3-2.8.4.pom\",\n    \"sha512\": \"e787ceb1edd6f4c520dd424de7b5455da0a9b57b53b24fc77647eabf9cbeda03506c21af12940bfe99a9899bc30e35fd5a4fc0cc5b50a235c5330162ea4a304c\",\n    \"dest\": \"offline-repository/androidx/room/room-rxjava3/2.8.4\",\n    \"dest-filename\": \"room-rxjava3-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-sqlite-wrapper/2.8.4/room-sqlite-wrapper-2.8.4.pom\",\n    \"sha512\": \"66e9bd58cdd0cb2fb6fe7ec6c67052e8090396ea821f2c264a46ca2e925d15ed0aefb7210fd4a959870ce8e466608350d67b1dea583726f3f65f97529adbbcf4\",\n    \"dest\": \"offline-repository/androidx/room/room-sqlite-wrapper/2.8.4\",\n    \"dest-filename\": \"room-sqlite-wrapper-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/androidx/room/room-testing/2.8.4/room-testing-2.8.4.pom\",\n    \"sha512\": \"8821f2fb82de6ff883ded9c6f5620d31dbced1b65f8d2e91d689e1effe615fa67564ee4c438641f792d38397418ec2f391f8d33bf64ebdc2c9cbd85749995eef\",\n    \"dest\": \"offline-repository/androidx/room/room-testing/2.8.4\",\n    \"dest-filename\": \"room-testing-2.8.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/application/com.android.application.gradle.plugin/8.13.2/com.android.application.gradle.plugin-8.13.2.pom\",\n    \"sha512\": \"ac7ab72b01a49144fc7b34e2f60a144120b4583bac09c087ff54397e08620ee76c2e34daf00ef3b67acc5d34b136f8f63eb51cd85d89083ab01731d01827ea0e\",\n    \"dest\": \"offline-repository/com/android/application/com.android.application.gradle.plugin/8.13.2\",\n    \"dest-filename\": \"com.android.application.gradle.plugin-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/8.13.2/baseLibrary-8.13.2.jar\",\n    \"sha512\": \"c1f350e35e37c065c887b52a4720186f96dfe87436791a61ea6424c094edd0d41435925f26b8433388608e197fe1c82a9596be0d366f28f3d3373f2c8bd2693a\",\n    \"dest\": \"offline-repository/com/android/databinding/baseLibrary/8.13.2\",\n    \"dest-filename\": \"baseLibrary-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/8.13.2/baseLibrary-8.13.2.pom\",\n    \"sha512\": \"8e5e92b2cf1f89dcd0f68319308323b9b71f517f1967faa1d255b32bae9525d270d5fdfa1f201b40abbcb0c8a5be65c1484007c24a447d5e076826bb32f93aa4\",\n    \"dest\": \"offline-repository/com/android/databinding/baseLibrary/8.13.2\",\n    \"dest-filename\": \"baseLibrary-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/kotlin/multiplatform/library/com.android.kotlin.multiplatform.library.gradle.plugin/8.13.2/com.android.kotlin.multiplatform.library.gradle.plugin-8.13.2.pom\",\n    \"sha512\": \"dc740260d9574c2786ce359d1c3ec0ed98817496c667535f9050b1c884d163951db08736e67124da6be62d508af6261a4d654e91ac9c79941d8e393692112a91\",\n    \"dest\": \"offline-repository/com/android/kotlin/multiplatform/library/com.android.kotlin.multiplatform.library.gradle.plugin/8.13.2\",\n    \"dest-filename\": \"com.android.kotlin.multiplatform.library.gradle.plugin-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/library/com.android.library.gradle.plugin/8.13.2/com.android.library.gradle.plugin-8.13.2.pom\",\n    \"sha512\": \"159dff919662124edc68968f340daaf51aadbd747c2569df4560d513e4c12e02d30a13880722d742ad2597bdcfa8239336f08d059c6b4f981a80e7b8f3d7cfe6\",\n    \"dest\": \"offline-repository/com/android/library/com.android.library.gradle.plugin/8.13.2\",\n    \"dest-filename\": \"com.android.library.gradle.plugin-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/signflinger/8.13.2/signflinger-8.13.2.jar\",\n    \"sha512\": \"17c42a17b67872ee819f94214d144ad08ce01ca66c323a6cecff46fb126177ab644a727551b90afa8df52f0eea2cbb8570d4be310ba1080199cce81ccb36926f\",\n    \"dest\": \"offline-repository/com/android/signflinger/8.13.2\",\n    \"dest-filename\": \"signflinger-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/signflinger/8.13.2/signflinger-8.13.2.pom\",\n    \"sha512\": \"dc05b9c6430f73618c5b2d2ca8da9cd939b6b642558debce58156d49a11353d344ade6aaa840aed15774abe4cc11632a3ec1334f7057bb261f612eba13ea4626\",\n    \"dest\": \"offline-repository/com/android/signflinger/8.13.2\",\n    \"dest-filename\": \"signflinger-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/31.13.2/crash-31.13.2.jar\",\n    \"sha512\": \"76bc17551f2225c42ca8b9277032f651b2f4835359a144569e154aeb0bc9b401259f9b7a5f4cb056a9a82b95e6313bdb5b8c3113259a36784b3913c17ddfd4a9\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/crash/31.13.2\",\n    \"dest-filename\": \"crash-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/31.13.2/crash-31.13.2.pom\",\n    \"sha512\": \"782f7e4f8ea9bc00d6a373802960a0e0f92e1827995246d4b9855ef973e770696d39ce78e85e23bf99db9431361e11cf681d1b4e967ed5d093acb8bddbfd3125\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/crash/31.13.2\",\n    \"dest-filename\": \"crash-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/31.13.2/protos-31.13.2.jar\",\n    \"sha512\": \"6c336bbb3125d75f6db312c74c92f2002b8b11561a9710f0454f11ba4382e51f7456fd06319f5272ec12e89615d9695589b45bf292d746463e62b0d2b1543bb0\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/protos/31.13.2\",\n    \"dest-filename\": \"protos-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/protos/31.13.2/protos-31.13.2.pom\",\n    \"sha512\": \"262d1288afc58d833011d9ebb2e016e7a121d03680d5fef5fbd27f07ffc02cbe5156c8a3c04a555810186c885be7290cd4d457861c6866297b8012fcc46859da\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/protos/31.13.2\",\n    \"dest-filename\": \"protos-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/31.13.2/shared-31.13.2.jar\",\n    \"sha512\": \"2d8c7250fdd64fa84994dd4a1b7a40b96eb2cd74fdb45007b092da9c2ed7f6d576a22aa772d5daac6577652767ee5c4708072f3975609c06413998ae1a1a664d\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/shared/31.13.2\",\n    \"dest-filename\": \"shared-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/shared/31.13.2/shared-31.13.2.pom\",\n    \"sha512\": \"95bb6af4ffe2b76321c1b1100678dd888d79fa40ebdbfc189c12c7b40776254bf01ab37f9e99e28e2521cd0255c5bc92c3fc85508fe4fc3a621472f1ab4fcc74\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/shared/31.13.2\",\n    \"dest-filename\": \"shared-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/31.13.2/tracker-31.13.2.jar\",\n    \"sha512\": \"6b59487d16cccdde6950425352f84198f7cc079ac639810f591a9bc0149fbd4199613b5c868c3aa3c9710e448ecdd395b4a8e9bc625511b45e7f9147555cce4f\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/tracker/31.13.2\",\n    \"dest-filename\": \"tracker-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/tracker/31.13.2/tracker-31.13.2.pom\",\n    \"sha512\": \"c3c223af6e18908a5c4f6c00c52ed314f5c4f4706b0924e8fd3eb7afdf04d4c5e3a34769d3951aab4d6dcec980337fe344273cf20b725280cc21ff5307a7cf77\",\n    \"dest\": \"offline-repository/com/android/tools/analytics-library/tracker/31.13.2\",\n    \"dest-filename\": \"tracker-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/annotations/31.13.2/annotations-31.13.2.jar\",\n    \"sha512\": \"a6ea020912ec68b3bfc20c3ca5df0627d5a3cf6cae544e7d8a25a7b71e7fbd6af722883ef3085a25d9669de47373a27fb281a4e677725ced788dabbfea6c8c77\",\n    \"dest\": \"offline-repository/com/android/tools/annotations/31.13.2\",\n    \"dest-filename\": \"annotations-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/annotations/31.13.2/annotations-31.13.2.pom\",\n    \"sha512\": \"7a13132257b182c844d59342610fa3fe0f5bc666af27ef61e4be32de1409eeaf0c68886e65cc8ba99986ff0e1963242339ecbdc9a5c9eecc80f5f36e89a7d1d3\",\n    \"dest\": \"offline-repository/com/android/tools/annotations/31.13.2\",\n    \"dest-filename\": \"annotations-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/8.13.2-14304508/aapt2-proto-8.13.2-14304508.jar\",\n    \"sha512\": \"015ad458966f24865fccdb016ae09f1ec83887c225e2a4063bd01cd6c7fcaf8f5a583d080547d9d806f2f4cfadea45304fd72dca1e30da7d75d6659214909950\",\n    \"dest\": \"offline-repository/com/android/tools/build/aapt2-proto/8.13.2-14304508\",\n    \"dest-filename\": \"aapt2-proto-8.13.2-14304508.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/8.13.2-14304508/aapt2-proto-8.13.2-14304508.module\",\n    \"sha512\": \"94a4d5d6278f5e2193d7dab17de46bc3f2289e7c249871ef114d6f2664970da0f8ab6b9415d6ad0847a17d87f8cd064fe2b5d5a43d66accb3f321a799b20a7e6\",\n    \"dest\": \"offline-repository/com/android/tools/build/aapt2-proto/8.13.2-14304508\",\n    \"dest-filename\": \"aapt2-proto-8.13.2-14304508.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2-proto/8.13.2-14304508/aapt2-proto-8.13.2-14304508.pom\",\n    \"sha512\": \"bc44d1170b5b3cd0f75876c546021da5e290a90c1eb4ce26255ad38f09db129c3eedd9f1d2d32819e4c3c8d87bca0f9be1890dbbfad740ca45fee73c6167612a\",\n    \"dest\": \"offline-repository/com/android/tools/build/aapt2-proto/8.13.2-14304508\",\n    \"dest-filename\": \"aapt2-proto-8.13.2-14304508.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/8.13.2/aaptcompiler-8.13.2.jar\",\n    \"sha512\": \"3167404cacdb8aa011cb2e8f6d326095c5bb811593169942b1afe27d84d66d00618cb403ac437538fee79b00d4762782ddd8aa955781bbd4462bf2d150d13b34\",\n    \"dest\": \"offline-repository/com/android/tools/build/aaptcompiler/8.13.2\",\n    \"dest-filename\": \"aaptcompiler-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/8.13.2/aaptcompiler-8.13.2.module\",\n    \"sha512\": \"f73f723c9c397aa5e1776351050000100d3003954ef34e809db7be2acfe158bc534efd507108ee88a6225de19dbb96d1b06ade17fd17bb497d8076d1db0f53db\",\n    \"dest\": \"offline-repository/com/android/tools/build/aaptcompiler/8.13.2\",\n    \"dest-filename\": \"aaptcompiler-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/aaptcompiler/8.13.2/aaptcompiler-8.13.2.pom\",\n    \"sha512\": \"16bf8ab4d06540a50d0bde15eb71e8198475b40f80881bae12ae62ecefb285c64fcae78b10d6e9dd0b931a9d1dc639f76144f7bcac102d318aa2a92d7d5b2473\",\n    \"dest\": \"offline-repository/com/android/tools/build/aaptcompiler/8.13.2\",\n    \"dest-filename\": \"aaptcompiler-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/8.13.2/apksig-8.13.2.jar\",\n    \"sha512\": \"a503e89577ad60e6109731e46b3d00fe4d04a3dd4971a88f1693f568f9bcec7a65c809b12dd750da1b68671e5fab45436538c1abfea37de84c7dc2f917acacd8\",\n    \"dest\": \"offline-repository/com/android/tools/build/apksig/8.13.2\",\n    \"dest-filename\": \"apksig-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/apksig/8.13.2/apksig-8.13.2.pom\",\n    \"sha512\": \"ee1f826990638478b92abfc70380b772580756efeb95bf587821d3e5fab1e2d786437ba1988a19905abf58ed12872fb269ca2420828baf916e3d997ee84a6253\",\n    \"dest\": \"offline-repository/com/android/tools/build/apksig/8.13.2\",\n    \"dest-filename\": \"apksig-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/8.13.2/apkzlib-8.13.2.jar\",\n    \"sha512\": \"6b728def0cdafe1be7c17c753cd279c864bfb01f48529a435028c66c054f7c56c330ad728b60c7a2ba6b054c8e81588904d39c80e62f301a1f6c754504c636d3\",\n    \"dest\": \"offline-repository/com/android/tools/build/apkzlib/8.13.2\",\n    \"dest-filename\": \"apkzlib-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/apkzlib/8.13.2/apkzlib-8.13.2.pom\",\n    \"sha512\": \"fe0d26662254eff21f551be4ed7ec5375014d0baf3a1d3d1e59d089def09081710c41be9632a50e4f2b6dbde7e86dfdb3d8e465771a4d770b5172f8608a8a715\",\n    \"dest\": \"offline-repository/com/android/tools/build/apkzlib/8.13.2\",\n    \"dest-filename\": \"apkzlib-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/8.13.2/builder-model-8.13.2.jar\",\n    \"sha512\": \"b386d3b5cd589c740d2b48d9d1269648cf03c8e5e8485060941ecdce48e2a91449a0d8a3d68a66720a7031c9756d16a3c313e4fd665d2d657879ce570b4cc7e9\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder-model/8.13.2\",\n    \"dest-filename\": \"builder-model-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/8.13.2/builder-model-8.13.2.module\",\n    \"sha512\": \"908a53d291884465fb153e80e2bdc70955eea58b2cee8a08a519205a2a845419e6617a12873e0c795eff41c0468712771cc410b44b7b7b12c8d134010fba5ec2\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder-model/8.13.2\",\n    \"dest-filename\": \"builder-model-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-model/8.13.2/builder-model-8.13.2.pom\",\n    \"sha512\": \"00524faa35b2bbaac535e8f19e2e1cd74312a541b0dd45bb48b8db47321612416fc4986cb27fed2abf77faa9c307d00a3849582e580c3d45ff366d9426ddb10d\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder-model/8.13.2\",\n    \"dest-filename\": \"builder-model-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/8.13.2/builder-test-api-8.13.2.jar\",\n    \"sha512\": \"25acd976393f36fecd3a689f043a8b5b85b4924a74866d437b4b87b86a4c684c4bb3d9d23167fc7abbeba4a7c57e34463459fb830b5dce874a4f95422dd24fee\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder-test-api/8.13.2\",\n    \"dest-filename\": \"builder-test-api-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/8.13.2/builder-test-api-8.13.2.module\",\n    \"sha512\": \"2809f9b7c30edc33ccfcd8b55b1478cc1aec67858c044ecdee9c6a4792908ed7b723df1261700f9520012e5d72474bf700c98b1106386492389294c529ddfe46\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder-test-api/8.13.2\",\n    \"dest-filename\": \"builder-test-api-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder-test-api/8.13.2/builder-test-api-8.13.2.pom\",\n    \"sha512\": \"47d00f07218086c04acbd2d7718e01ac5f870c9653179c060a669028c6eedcff5c7fee7d3aff69d081be1d9f0dc6a827c7962b3722bb72df04b1aa88d059d0b3\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder-test-api/8.13.2\",\n    \"dest-filename\": \"builder-test-api-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/8.13.2/builder-8.13.2.jar\",\n    \"sha512\": \"89b08d1750d73cf45f1e6f7f2030b2b702eff7a3da88f3a9a9550a3641e280c5a3f1d506020d10fbcb6258063aeb858eeee955d68086f4d386d248a635dcbdda\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder/8.13.2\",\n    \"dest-filename\": \"builder-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/8.13.2/builder-8.13.2.module\",\n    \"sha512\": \"80240214f0f02df3f72971fb4f76cd5e620b55638b58291c02eb487f774ea5a726540b62fc9657a737ac61b363b96e26ed52d274ef2085f99fd615664872cf4c\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder/8.13.2\",\n    \"dest-filename\": \"builder-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/builder/8.13.2/builder-8.13.2.pom\",\n    \"sha512\": \"0cff48e01c5cb39053935310b2c0d8a24bd1f20cba81b97abdd23530dae67455658b5d602ae2e1b8c67dd938072502eab8b56be69c35d0b436172cef8a1458a8\",\n    \"dest\": \"offline-repository/com/android/tools/build/builder/8.13.2\",\n    \"dest-filename\": \"builder-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.18.1/bundletool-1.18.1.jar\",\n    \"sha512\": \"8d05cabd33fc433a40cc275d32269c188b13cfe251bc6ccc98d3937903c94f4d1209f2b684fde6548730ff999e069cd3a07969aa2f791a3653c5546f36b5f746\",\n    \"dest\": \"offline-repository/com/android/tools/build/bundletool/1.18.1\",\n    \"dest-filename\": \"bundletool-1.18.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/bundletool/1.18.1/bundletool-1.18.1.pom\",\n    \"sha512\": \"7d40fc5b3be06b8bc4a8db2f778ab22c20c5f433ac8a53d5fef4d14ad3a8cfb97e09e8d9391778b20dfdee7653ca3e6d9dd27508ab1475819c316dc1f28d5f9e\",\n    \"dest\": \"offline-repository/com/android/tools/build/bundletool/1.18.1\",\n    \"dest-filename\": \"bundletool-1.18.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/8.13.2/gradle-api-8.13.2.jar\",\n    \"sha512\": \"f77040e938668ef7a345d6eae6ff32248fa5ea994c6eb2f8c5c7f4eec3120fb589e29d9ef4b7c8b5469fc609c8614ce9af8ee8a1b7d1a6e236575c6d1cb7ad30\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-api/8.13.2\",\n    \"dest-filename\": \"gradle-api-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/8.13.2/gradle-api-8.13.2.module\",\n    \"sha512\": \"835ed000e80241b81bf169ccd66d55759ae06aa155134a47a2505d722b67887e4b86c9331208e2a648dd211c129596033b3542648a12fba768a63176d08b7d1d\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-api/8.13.2\",\n    \"dest-filename\": \"gradle-api-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-api/8.13.2/gradle-api-8.13.2.pom\",\n    \"sha512\": \"cab29894d2ddaef258d4e352b07b8e9560f5215e220be5269b8d3ad61253dce3a8183eeccea681f8b37dd9f0d9dc24e86affbe4acc7b0ddbd8d9ef67f4aab24e\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-api/8.13.2\",\n    \"dest-filename\": \"gradle-api-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-common-api/8.13.2/gradle-common-api-8.13.2.jar\",\n    \"sha512\": \"ce61cb68f9f9fc41752f78c0786b9bb3ac1adf4d838ec053a51cc7e3e9bbc7a517b3c091e1a86a4fe7259e2bbe6ca753231373d047d4f77dc0561a412045fa39\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-common-api/8.13.2\",\n    \"dest-filename\": \"gradle-common-api-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-common-api/8.13.2/gradle-common-api-8.13.2.module\",\n    \"sha512\": \"1d041eed1b285fb767413324bfdff6b1daee758f0880a69ab26f3808523bb91d95c8345f5aad665bef4771dfac588b14704929b4bb7ddf1ffb0783fb69170c7a\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-common-api/8.13.2\",\n    \"dest-filename\": \"gradle-common-api-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-common-api/8.13.2/gradle-common-api-8.13.2.pom\",\n    \"sha512\": \"4cb11d9bf5cce75b6d4f21f5301d96d03cc5ccdcb4851f5e6b63a4cf3d26cbef2e859ef38dead51654a800d55d47a50c3e1a8ae8e2ab6c5ca55d20e48611648e\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-common-api/8.13.2\",\n    \"dest-filename\": \"gradle-common-api-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-settings-api/8.13.2/gradle-settings-api-8.13.2.jar\",\n    \"sha512\": \"aca15ccad0416a52345fc66ff0a766c650786da87b0461cf446fe866ab74eba3c793b0b376163f1a543ec122aea4d64a5f2c7e3ec1b2a36cbf4769f1f488de2b\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-settings-api/8.13.2\",\n    \"dest-filename\": \"gradle-settings-api-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-settings-api/8.13.2/gradle-settings-api-8.13.2.module\",\n    \"sha512\": \"580b86742978482e4f8678dddbdf7f8b8c7b33d5b731edbe68fca56e9ef2adb21875bc32c9b004c655b80b29196f25f1a3d39ca8f0720a121fc59d9d5bc6ee13\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-settings-api/8.13.2\",\n    \"dest-filename\": \"gradle-settings-api-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle-settings-api/8.13.2/gradle-settings-api-8.13.2.pom\",\n    \"sha512\": \"f6954fac8f5ad38c82122e50dea3867efccf95fee238dcde7e0e14338fed50ab357196ff9c8f8db8a656b45f5ada5713f5d50322d08ffbe84245f42e639c6deb\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle-settings-api/8.13.2\",\n    \"dest-filename\": \"gradle-settings-api-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/8.13.2/gradle-8.13.2.jar\",\n    \"sha512\": \"11d0905bb50514dc9800c05da7ea80ddefe473ff46aeb8dcfb797e548e8b8c7b5609ff8fc879346b3ab4a37c93f39ccabc7601c5f4775c7abb8326cb8062f182\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle/8.13.2\",\n    \"dest-filename\": \"gradle-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/8.13.2/gradle-8.13.2.module\",\n    \"sha512\": \"a48c63cf8fdaa37eae2e2a0ab601c34bce2a91783c2266c7a96c1ce4044626c877bfaa7a2543ab4303e22f939856df060451bc934c5c8245f2ba5762399c5c84\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle/8.13.2\",\n    \"dest-filename\": \"gradle-8.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/8.13.2/gradle-8.13.2.pom\",\n    \"sha512\": \"ee02d56bbb4bf65003c74a510fcfee13c8c45667ba7213847b0666e70995c43c3111bedb03ef7fc0d1c7c87b13491ba1804ea00c68b5e30d0af773d64379c6d0\",\n    \"dest\": \"offline-repository/com/android/tools/build/gradle/8.13.2\",\n    \"dest-filename\": \"gradle-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10/jetifier-core-1.0.0-beta10.jar\",\n    \"sha512\": \"7c87414a0753a7bc5e14c34acb9bec7911960ce7832e8b78825c0fa516631b4687b839b50ca938b75fbe439636b9a2823303b68cc52d36e9559ae750caa978fc\",\n    \"dest\": \"offline-repository/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10\",\n    \"dest-filename\": \"jetifier-core-1.0.0-beta10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10/jetifier-core-1.0.0-beta10.module\",\n    \"sha512\": \"30592727ce41feb2cf3eef2cc59e06a9cf838cd65f724fc65d7dc249c3a89f16e6a21ef68a7751739bbbf7656a7ddaadac37395583d7fa3f574eb1ec57ec5bdc\",\n    \"dest\": \"offline-repository/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10\",\n    \"dest-filename\": \"jetifier-core-1.0.0-beta10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10/jetifier-core-1.0.0-beta10.pom\",\n    \"sha512\": \"1dacabd19fe8e2b5be6bd1028f7b10c2100873a599f197c2e63601ac23774e192e0612bc3eede9754e5c38d3a7439f4be781670314af070daa4c0eaba51c58ef\",\n    \"dest\": \"offline-repository/com/android/tools/build/jetifier/jetifier-core/1.0.0-beta10\",\n    \"dest-filename\": \"jetifier-core-1.0.0-beta10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10/jetifier-processor-1.0.0-beta10.jar\",\n    \"sha512\": \"5f7316f3fbb67ef1c8993be3343a8ae0e6e1363053c306beb43a4863185b8b614ea76f1b36e4d1acfbe4e8643f48af214ea2f988d7f53b9af088dbaed6a4987c\",\n    \"dest\": \"offline-repository/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10\",\n    \"dest-filename\": \"jetifier-processor-1.0.0-beta10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10/jetifier-processor-1.0.0-beta10.module\",\n    \"sha512\": \"6485aeafd7b53471c4a5424112eb9031fc890d365a6813489b3c671c4d0b52ddfbaab07f2e522a11f1e2112f011bac422086a70bca639139197c2e360e078fbc\",\n    \"dest\": \"offline-repository/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10\",\n    \"dest-filename\": \"jetifier-processor-1.0.0-beta10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10/jetifier-processor-1.0.0-beta10.pom\",\n    \"sha512\": \"7969cb4ccc5c767cb17af91911d6169a43017b2f352ee4bf02de25abcd882a4617386916d2218dae52769e02a38704616ace9f1f5b34fa5ff6f48142f9fa00ea\",\n    \"dest\": \"offline-repository/com/android/tools/build/jetifier/jetifier-processor/1.0.0-beta10\",\n    \"dest-filename\": \"jetifier-processor-1.0.0-beta10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/31.13.2/manifest-merger-31.13.2.jar\",\n    \"sha512\": \"612cf2b0830a435d4e2d35b3d4d1ffbb89b3e161f840222f949a464a8a83d8ac619d2a8ce5fb18f56d8364d923a6522cfe0c078b7f83aad28f2b9bc2912365fc\",\n    \"dest\": \"offline-repository/com/android/tools/build/manifest-merger/31.13.2\",\n    \"dest-filename\": \"manifest-merger-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/31.13.2/manifest-merger-31.13.2.module\",\n    \"sha512\": \"357fb7573ec3574c05e2d631e086832f837ca8812967a06006defbfc31cbf1d673f395c0e695ca39fe1ffcbfe5bb9dbe6575e12d66d72aecc0a87bf35ccac0d1\",\n    \"dest\": \"offline-repository/com/android/tools/build/manifest-merger/31.13.2\",\n    \"dest-filename\": \"manifest-merger-31.13.2.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/manifest-merger/31.13.2/manifest-merger-31.13.2.pom\",\n    \"sha512\": \"1919d0ccde2285c31e3fef58edc07b447a68c53ab12ea5a287f1e8f78a3ba682d0446584b649c01b5cf77990f6494d08f64e4a704c248a977a8a8b0231ab1fab\",\n    \"dest\": \"offline-repository/com/android/tools/build/manifest-merger/31.13.2\",\n    \"dest-filename\": \"manifest-merger-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/transform-api/2.0.0-deprecated-use-gradle-api/transform-api-2.0.0-deprecated-use-gradle-api.jar\",\n    \"sha512\": \"f7af126566e550c43586db59bb020a2f85387d4e60b533b2a04f988e75350584cc557e49ea3f1237327e487e2a50de98196514eb218c6ce83661d2291d2cde54\",\n    \"dest\": \"offline-repository/com/android/tools/build/transform-api/2.0.0-deprecated-use-gradle-api\",\n    \"dest-filename\": \"transform-api-2.0.0-deprecated-use-gradle-api.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/build/transform-api/2.0.0-deprecated-use-gradle-api/transform-api-2.0.0-deprecated-use-gradle-api.pom\",\n    \"sha512\": \"d0b9a08d477c7e59ed723868ec676b24ba6043038c4f0b4a31368fb3a55e39b69e08f666142f10b2a55aec8f64d9435be0ea9fe039a5fb0a09ca88e4085a21a6\",\n    \"dest\": \"offline-repository/com/android/tools/build/transform-api/2.0.0-deprecated-use-gradle-api\",\n    \"dest-filename\": \"transform-api-2.0.0-deprecated-use-gradle-api.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/common/31.13.2/common-31.13.2.jar\",\n    \"sha512\": \"23af8b9e44d83cde8a5e662ceab722baec1b03264d96f96324cbc983ce525280ba9dc021d745f46f657aa11867943199cbf186b5d1992e902177c5a4c6635781\",\n    \"dest\": \"offline-repository/com/android/tools/common/31.13.2\",\n    \"dest-filename\": \"common-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/common/31.13.2/common-31.13.2.pom\",\n    \"sha512\": \"c5051378f28e42c99eb6597b7e23aee6f1e6aaafd4953cc0416616230a28edc5fb88b40fa410edf89c7210ec48fe7f9af5d408c86f3aea0cbe47168bf535bb59\",\n    \"dest\": \"offline-repository/com/android/tools/common/31.13.2\",\n    \"dest-filename\": \"common-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/31.13.2/ddmlib-31.13.2.jar\",\n    \"sha512\": \"7555895ae6ef80361efbbb46f322f9e9f14eefffebef2383f2621882d7e9dd977c72f390cf6b53dbb985d841c8996aecedf7598c741f8692addeb5ede5089e31\",\n    \"dest\": \"offline-repository/com/android/tools/ddms/ddmlib/31.13.2\",\n    \"dest-filename\": \"ddmlib-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/ddms/ddmlib/31.13.2/ddmlib-31.13.2.pom\",\n    \"sha512\": \"1d9dc37f752a76e9329de81b57310966a4d5e71ecfb065bb711e143327ea6ad8ae1d7f73d1994237a7372524d530aebb71880f7b801b4d36fa79be1a94d1326a\",\n    \"dest\": \"offline-repository/com/android/tools/ddms/ddmlib/31.13.2\",\n    \"dest-filename\": \"ddmlib-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/31.13.2/dvlib-31.13.2.jar\",\n    \"sha512\": \"3ffe3b1b18bcedd4aa6c4d774ce10ba19574f34290cc56c921f4c6d8d69c19f4346d5ec1ff1a294444ede3a5276402de9dd2782b412cced5a0cba8f73407797b\",\n    \"dest\": \"offline-repository/com/android/tools/dvlib/31.13.2\",\n    \"dest-filename\": \"dvlib-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/dvlib/31.13.2/dvlib-31.13.2.pom\",\n    \"sha512\": \"7e3e845fe1c49effdb29c9d4c658d100466b3e712d9e90c5d019bcd78a962ecfabb302772a3af2a1bffe90ce7672653bed49431166529a3a5696c1ab97a6e397\",\n    \"dest\": \"offline-repository/com/android/tools/dvlib/31.13.2\",\n    \"dest-filename\": \"dvlib-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/31.13.2/layoutlib-api-31.13.2.jar\",\n    \"sha512\": \"3a177db40cf73661e1e640391fe54eed921314c666e22d749e1b8148eb1ec83818d52fbaad26df8ad1620194ec51aae76db8af04a4e963d46d6bafa43e842ab9\",\n    \"dest\": \"offline-repository/com/android/tools/layoutlib/layoutlib-api/31.13.2\",\n    \"dest-filename\": \"layoutlib-api-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/layoutlib/layoutlib-api/31.13.2/layoutlib-api-31.13.2.pom\",\n    \"sha512\": \"8f71ef881efb5780ef6062821cc284f0926d452c4b4b7e04a6d090dd93996b75b6bca3bcd350088a282414311ac8515eb4111703c7c8ba0acdc4ec9d3e4e5f0f\",\n    \"dest\": \"offline-repository/com/android/tools/layoutlib/layoutlib-api/31.13.2\",\n    \"dest-filename\": \"layoutlib-api-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/31.13.2/lint-model-31.13.2.jar\",\n    \"sha512\": \"bf2e3bee36c76d53fa6d7d5f83af53e4ff8eb37eb5e83657fa3d10bb87825d2240ddd226428573ff44873f025d6fab78a81eb8ead649442357d580530c07c1b7\",\n    \"dest\": \"offline-repository/com/android/tools/lint/lint-model/31.13.2\",\n    \"dest-filename\": \"lint-model-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-model/31.13.2/lint-model-31.13.2.pom\",\n    \"sha512\": \"b8888bebc6fd338cb23e6eccd3528544d5de645be5208587f537efd33706dad2d3dd0dcd2e558b0f2f36f324a4f139ac0e1733b20ad1ec44d5c91f1e73f6771e\",\n    \"dest\": \"offline-repository/com/android/tools/lint/lint-model/31.13.2\",\n    \"dest-filename\": \"lint-model-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/31.13.2/lint-typedef-remover-31.13.2.jar\",\n    \"sha512\": \"a715c531216f0f5d23b0a78342997452c0b19c4f396bdb631a009efbfbf3e114c25933110350806493b2d77db907f07033096aafc3d48410fb5486106391697f\",\n    \"dest\": \"offline-repository/com/android/tools/lint/lint-typedef-remover/31.13.2\",\n    \"dest-filename\": \"lint-typedef-remover-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/lint/lint-typedef-remover/31.13.2/lint-typedef-remover-31.13.2.pom\",\n    \"sha512\": \"592d7b49796425b53c329bd86658e1c10250b2bcd7175f01c9d6867f1b72ad46b22cba66c57099a6efa8c527e74404bef517907140a159152646d8fa8c09d7dc\",\n    \"dest\": \"offline-repository/com/android/tools/lint/lint-typedef-remover/31.13.2\",\n    \"dest-filename\": \"lint-typedef-remover-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/repository/31.13.2/repository-31.13.2.jar\",\n    \"sha512\": \"0aa3faca5a5c60e1c97edba92af8dd2d7529d50e2c5dbf38af39d5569acba2a754406b298248a7f30ff37a391a0243c749534c65de06994ecbdb3ad833ba2891\",\n    \"dest\": \"offline-repository/com/android/tools/repository/31.13.2\",\n    \"dest-filename\": \"repository-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/repository/31.13.2/repository-31.13.2.pom\",\n    \"sha512\": \"7936e6a53d51c08f5ba01f96f26083cc192747e22633969b12b760f4051c3007d3def9483e8dd6bae82f87a961b3f09c903131664a9a5866e91ac7874cd26bff\",\n    \"dest\": \"offline-repository/com/android/tools/repository/31.13.2\",\n    \"dest-filename\": \"repository-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/31.13.2/sdk-common-31.13.2.jar\",\n    \"sha512\": \"f172c3ce6668721b96aa2e076605015f2d07290a9e365ca5b960f25d46aa72382faff1b3a75b995462e15ef613529d4c540338c406d5f5885e530e1198925e8f\",\n    \"dest\": \"offline-repository/com/android/tools/sdk-common/31.13.2\",\n    \"dest-filename\": \"sdk-common-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/sdk-common/31.13.2/sdk-common-31.13.2.pom\",\n    \"sha512\": \"1423934bff3a691d8fecf71a1742ea538783fcce0a39dfed4bb1f9583a6c5e2b67bb235fc9702137bc10400a38a2b4508cf757b075ad8c89b7b78566cf6edb49\",\n    \"dest\": \"offline-repository/com/android/tools/sdk-common/31.13.2\",\n    \"dest-filename\": \"sdk-common-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/31.13.2/sdklib-31.13.2.jar\",\n    \"sha512\": \"cf07a4d842aa87c04cbc225c4f747479bc67eab9fb6825db37b6087a425006f73830b66b46d96f521644414aa3ba133205f86da197295eef52cd0d153f1c2e91\",\n    \"dest\": \"offline-repository/com/android/tools/sdklib/31.13.2\",\n    \"dest-filename\": \"sdklib-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/sdklib/31.13.2/sdklib-31.13.2.pom\",\n    \"sha512\": \"3e177dd693e37c509d8b3231bd809af4a26d18a90f1448bbfb5977e29e30d4398da0fff30266d4bcfc5d022a825c2e591b83aa6d987cb06494c170a2ae87f881\",\n    \"dest\": \"offline-repository/com/android/tools/sdklib/31.13.2\",\n    \"dest-filename\": \"sdklib-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/31.13.2/android-device-provider-ddmlib-proto-31.13.2.jar\",\n    \"sha512\": \"1ccf5d72b1137a2e1fd76363d5dff75821fab87bb83db1ce19e8052a453aa0594666e3ae858d0c720a22483ee8b2f0971dba45e22f7907de22468a35997090a6\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-device-provider-ddmlib-proto/31.13.2\",\n    \"dest-filename\": \"android-device-provider-ddmlib-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-ddmlib-proto/31.13.2/android-device-provider-ddmlib-proto-31.13.2.pom\",\n    \"sha512\": \"e6d2ab25509e43367b799bf96ab971a04c7cbbbf33d94d5ef49df39f483554964273c3a5efafa5d2710f617573c5a03ffcd1ce581cfef81a339ce52fe69bd582\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-device-provider-ddmlib-proto/31.13.2\",\n    \"dest-filename\": \"android-device-provider-ddmlib-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-profile-proto/31.13.2/android-device-provider-profile-proto-31.13.2.jar\",\n    \"sha512\": \"f01ddf4e7884fb9811bb8c6a65641617e28cb7d5457ab99740522cf83f49dfe129c1e85e41ec7c71a76ef626d08311c8b2c2603c7b0da7c66db14365204df50e\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-device-provider-profile-proto/31.13.2\",\n    \"dest-filename\": \"android-device-provider-profile-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-device-provider-profile-proto/31.13.2/android-device-provider-profile-proto-31.13.2.pom\",\n    \"sha512\": \"ec048731e474567a8bce094000e0d2b7831266dbe10ef1ab6ac1a159fefe10236efec3cba5e6c67ae1ff3936d6a369cd90cd06d37be207b71915070c51d87088\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-device-provider-profile-proto/31.13.2\",\n    \"dest-filename\": \"android-device-provider-profile-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.13.2/android-test-plugin-host-additional-test-output-proto-31.13.2.jar\",\n    \"sha512\": \"27057dd8cf1af00835f478ed518d3ce0ec1e94080879464996bf2e718ad640177c68a6ab542f5df6b3c8ef86de4e3d3cc7a150b090f06516758c20f9e564177b\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-additional-test-output-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.13.2/android-test-plugin-host-additional-test-output-proto-31.13.2.pom\",\n    \"sha512\": \"5097c0187dc392e1dc6f1c72eba44d832d63f6ca03c511722ac70c48d47d30459091467f735c97f9f28f3bc275166056fbf92f2f1f8c8b6feb4aac7592c2df11\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-additional-test-output-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-additional-test-output-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.13.2/android-test-plugin-host-apk-installer-proto-31.13.2.jar\",\n    \"sha512\": \"58407e505a8970015374adaeeca50647915e301f948f9260375daf17b90a7bf9228634fb691eeedbdeb4af1519bf2ffc0888bf26f5490115e2a72879c12c8099\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-apk-installer-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.13.2/android-test-plugin-host-apk-installer-proto-31.13.2.pom\",\n    \"sha512\": \"5aa8f5655a8259646a3802e6ecfc4448bdbe5669764df86f1dfdba83cacedb35dc34867f67aad6fd63f6863a3402b6e595f2a9fb062a0835648b0e80f4442788\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-apk-installer-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-apk-installer-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/31.13.2/android-test-plugin-host-coverage-proto-31.13.2.jar\",\n    \"sha512\": \"4b577cd576b15a72219c8fec1f991808ae67711c07e1cf3e28ef8450fcacdabd909bc4c417f3b66fe991165b2a8d22a6a927b0247f39f314add9b040b3e5dd5f\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-coverage-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-coverage-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-coverage-proto/31.13.2/android-test-plugin-host-coverage-proto-31.13.2.pom\",\n    \"sha512\": \"831f7b879fff16741c00a4d6df465f9cf2a52d4ad8ce50cf791bf24e897ad77a7240f7546a4cec4da0cecb6ef454b29908f0453dda484725743372e79e4f9481\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-coverage-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-coverage-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.13.2/android-test-plugin-host-emulator-control-proto-31.13.2.jar\",\n    \"sha512\": \"421d48cec745881a9634f65321bf215a518b67be1c420d3f2b9a243b95a8891117d62b3a6e1bb6a3edf8a7ce19ac1522240d93e8f82e83e7fbfe5e88c2d0d457\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-emulator-control-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.13.2/android-test-plugin-host-emulator-control-proto-31.13.2.pom\",\n    \"sha512\": \"84e867844d3605f0f4a64cbeb930771a11a392da654164b13a4e34a5b55e092ba194fea7376d883cb5245afd8f19374f13f914a654beadb517963686666675ba\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-emulator-control-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-emulator-control-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-logcat-proto/31.13.2/android-test-plugin-host-logcat-proto-31.13.2.jar\",\n    \"sha512\": \"1d2161091825c9841a99b38970314f932fa8c0e0a3f132afb0e2290700388a5487564ebadb42a1c4bb20a71c138058dfff6fe50dfd026172f7337f45ea46d466\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-logcat-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-logcat-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-host-logcat-proto/31.13.2/android-test-plugin-host-logcat-proto-31.13.2.pom\",\n    \"sha512\": \"d6578f73ae9a1c922bee4e09ae54784771a08e4f8cc859b7bc12c83334794e63fa593e1e978ab768d767852047271b5af865562a61f55bcd437b1f4e900929e0\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-host-logcat-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-host-logcat-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.13.2/android-test-plugin-result-listener-gradle-proto-31.13.2.jar\",\n    \"sha512\": \"19e764c50ecd46d33e05c8dfd1917e9bdb782725bf572e014ef58d597c049e1531a594712608177006c51066768909a5f050d4224dfcd626027d944fe81cad8a\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-result-listener-gradle-proto-31.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.13.2/android-test-plugin-result-listener-gradle-proto-31.13.2.pom\",\n    \"sha512\": \"9656f5670d085e081bb2c94f3132e9ebe1f4045898a4cdedd5d6fb441aa9bd60628a0b42bc4a0f5624d6eb6b060600d0ed90d78be152c48757a4c9e4fb6e4206\",\n    \"dest\": \"offline-repository/com/android/tools/utp/android-test-plugin-result-listener-gradle-proto/31.13.2\",\n    \"dest-filename\": \"android-test-plugin-result-listener-gradle-proto-31.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/zipflinger/8.13.2/zipflinger-8.13.2.jar\",\n    \"sha512\": \"d101991a9ab84eb9b116a29ba24c8a1d36541f7fe94c27c6fe3658c31ab82f1aa9807e5f54fb4e2776e06658bf2f59a5384513ff3293b578bba257f2aa367c71\",\n    \"dest\": \"offline-repository/com/android/zipflinger/8.13.2\",\n    \"dest-filename\": \"zipflinger-8.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/android/zipflinger/8.13.2/zipflinger-8.13.2.pom\",\n    \"sha512\": \"b8378dde5097e6551034c363b94603218f8c9b8f86e6a611ac50db6d2e548f81a7895afee54e958371202a22c0cfa15b5bd5f842ad3d1560d514305da59cff15\",\n    \"dest\": \"offline-repository/com/android/zipflinger/8.13.2\",\n    \"dest-filename\": \"zipflinger-8.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/android/annotations/4.1.1.4/annotations-4.1.1.4.jar\",\n    \"sha512\": \"530bfa9e7aea7b2dc2e8776f083705f12772045f6f4bbe235a1c3e97646bd0b0a367358aab0f129058d1899573f4bce97d7db3dfff96dfdabc99377c5d837222\",\n    \"dest\": \"offline-repository/com/google/android/annotations/4.1.1.4\",\n    \"dest-filename\": \"annotations-4.1.1.4.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/android/annotations/4.1.1.4/annotations-4.1.1.4.pom\",\n    \"sha512\": \"6727ecc1bc63112555f4ff8013d0b136f44641d73673e03e89ccb45d6b918c05717ff32065088d72e94b7b6600e9536b83e96faa842bcb837cb34aff7eebe88e\",\n    \"dest\": \"offline-repository/com/google/android/annotations/4.1.1.4\",\n    \"dest-filename\": \"annotations-4.1.1.4.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/api/grpc/proto-google-common-protos/2.48.0/proto-google-common-protos-2.48.0.jar\",\n    \"sha512\": \"201eea4fa42a6c109ff14af3a8bb4f38de46442d4ecf6ee2b4ac116e4b91fff88127f6eb2acebf6d86afca6db67cbed3aca2a0132a05f56e6151206b5598fdb8\",\n    \"dest\": \"offline-repository/com/google/api/grpc/proto-google-common-protos/2.48.0\",\n    \"dest-filename\": \"proto-google-common-protos-2.48.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/api/grpc/proto-google-common-protos/2.48.0/proto-google-common-protos-2.48.0.pom\",\n    \"sha512\": \"0b2562159ce9019052cc45a7e4ccdb4ae28e36a5a83ef641cd4316566fdd66fcf5906c03c10a1f03c12d3d63607537211f26cde290e6553817285981428293ac\",\n    \"dest\": \"offline-repository/com/google/api/grpc/proto-google-common-protos/2.48.0\",\n    \"dest-filename\": \"proto-google-common-protos-2.48.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/auto/auto-parent/6/auto-parent-6.pom\",\n    \"sha512\": \"5766f2b3a848a39440ff27d6985c6a88500c97511ec3634d53d547e53ad58c944a6f73128dc3f4e8286972b6161e65d46517694a2dbefcab18bb966ed7011492\",\n    \"dest\": \"offline-repository/com/google/auto/auto-parent/6\",\n    \"dest-filename\": \"auto-parent-6.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.6.2/auto-value-annotations-1.6.2.jar\",\n    \"sha512\": \"37fc4689f2885261fecee345300934025fd65b9e43a65a696c9046c3d2919c1c09f30dcbe0b74099f9307a5248dd08030edd4233637e3db3ab40a16b30cb23b4\",\n    \"dest\": \"offline-repository/com/google/auto/value/auto-value-annotations/1.6.2\",\n    \"dest-filename\": \"auto-value-annotations-1.6.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-annotations/1.6.2/auto-value-annotations-1.6.2.pom\",\n    \"sha512\": \"d9c83a387f0b6285fbcda3ba32dc6e888f2876a3eaec9a0a15c04517466f72c449784e5829503fb74ae988e353829709f6b2f7467b0450a942ed33caa96380ab\",\n    \"dest\": \"offline-repository/com/google/auto/value/auto-value-annotations/1.6.2\",\n    \"dest-filename\": \"auto-value-annotations-1.6.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/auto/value/auto-value-parent/1.6.2/auto-value-parent-1.6.2.pom\",\n    \"sha512\": \"1c467407114d4eda18a71eca2093f6439b1bc7ed0c2e4f01df1975d45b7b261fb7aee09376b35f3c9928498b527c9ea4b1e9622b18e3478bf481fd64947db978\",\n    \"dest\": \"offline-repository/com/google/auto/value/auto-value-parent/1.6.2\",\n    \"dest-filename\": \"auto-value-parent-1.6.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar\",\n    \"sha512\": \"bb09db62919a50fa5b55906013be6ca4fc7acb2e87455fac5eaf9ede2e41ce8bbafc0e5a385a561264ea4cd71bbbd3ef5a45e02d63277a201d06a0ae1636f804\",\n    \"dest\": \"offline-repository/com/google/code/findbugs/jsr305/3.0.2\",\n    \"dest-filename\": \"jsr305-3.0.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.pom\",\n    \"sha512\": \"08e1cc341a153f64b670d87831eedfe79a150b8fb7e3a4afbaef54deaa28d2767ae86d12b4f0c5404184360ab8e48a3655e610f3bf2fe6c97a06e9fc3df49b37\",\n    \"dest\": \"offline-repository/com/google/code/findbugs/jsr305/3.0.2\",\n    \"dest-filename\": \"jsr305-3.0.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/code/gson/gson-parent/2.13.2/gson-parent-2.13.2.pom\",\n    \"sha512\": \"e6524dbb8aa13707fa51df7413a8b247a4de248812e212702e4e5888d0df29c6ebe39a3b1e09aefb12a1a095c42d19d074756a30918bfc4c1fe55c39b9a7c1fa\",\n    \"dest\": \"offline-repository/com/google/code/gson/gson-parent/2.13.2\",\n    \"dest-filename\": \"gson-parent-2.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.2/gson-2.13.2.jar\",\n    \"sha512\": \"8974a052656d2e5ec968b6bac2edf51413ffc62040fdc65f14f00597e738ab544d22487f8579ba90618b5a7f94ef33773510fac67b408fee6ed274b38f3d9947\",\n    \"dest\": \"offline-repository/com/google/code/gson/gson/2.13.2\",\n    \"dest-filename\": \"gson-2.13.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.2/gson-2.13.2.pom\",\n    \"sha512\": \"c0d089089bf0d6bcfae008f329dcc2db2538d02654f8bc7690cb8db2ec94fe0d2aa069ac1d70a111a28cced0ffd7742ac8db29a884d64d9413ebf9b2af5dab82\",\n    \"dest\": \"offline-repository/com/google/code/gson/gson/2.13.2\",\n    \"dest-filename\": \"gson-2.13.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/crypto/tink/tink/1.7.0/tink-1.7.0.jar\",\n    \"sha512\": \"bcc1da6501249bdcd4b8053b7220175ed922887240358ed6e6846b1cfdba813ee9c854877065b3e9540a4c456eccd5841d5a485eee6d0228cf838f2b9cce1037\",\n    \"dest\": \"offline-repository/com/google/crypto/tink/tink/1.7.0\",\n    \"dest-filename\": \"tink-1.7.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/crypto/tink/tink/1.7.0/tink-1.7.0.pom\",\n    \"sha512\": \"1dd30bd9ba5060dee08f35a88358bbd009499448d9d595a1a04afb932a785531d78b514a39e404f3e95f5c06ffa6b2c26a675b12175ae944ca08c811ee649074\",\n    \"dest\": \"offline-repository/com/google/crypto/tink/tink/1.7.0\",\n    \"dest-filename\": \"tink-1.7.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/dagger/dagger/2.28.3/dagger-2.28.3.jar\",\n    \"sha512\": \"0ab9b4b48db3393127c4979f7f9ed9af63a1d9ceab8eade10ba9c6a7f480204c9951740fe400e8940afb28089d4915ab8670565cd381c70d4b10ffb65e3383dc\",\n    \"dest\": \"offline-repository/com/google/dagger/dagger/2.28.3\",\n    \"dest-filename\": \"dagger-2.28.3.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/dagger/dagger/2.28.3/dagger-2.28.3.pom\",\n    \"sha512\": \"51cd57eec1b311890e7d7fb3706a7202f7fcb2252e91f8979800f48225ce8cfd429811c95c9ea05d3a7fb6519a61279604dc6c1d27666da10d8a6d61747477de\",\n    \"dest\": \"offline-repository/com/google/dagger/dagger/2.28.3\",\n    \"dest-filename\": \"dagger-2.28.3.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/com.google.devtools.ksp.gradle.plugin/2.3.5/com.google.devtools.ksp.gradle.plugin-2.3.5.pom\",\n    \"sha512\": \"113c90ac4da3a1d6a38942565e8913db13b91c2e6d51144f5843e021ef384f2efa47cf859eb248c42cd8b824ddc1741bf2959c950795619c18566b92bbc4e11b\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/com.google.devtools.ksp.gradle.plugin/2.3.5\",\n    \"dest-filename\": \"com.google.devtools.ksp.gradle.plugin-2.3.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-api/2.3.5/symbol-processing-api-2.3.5.jar\",\n    \"sha512\": \"c72ae0d465aa7b931b8e778205eb73a05df12c9fc0658ecbf74fee0a5b14f5aa1c0db8ed61a400d21e171898a0aa75540c2e75367e831c30939aa4a2a7999226\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-api/2.3.5\",\n    \"dest-filename\": \"symbol-processing-api-2.3.5.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-api/2.3.5/symbol-processing-api-2.3.5.module\",\n    \"sha512\": \"8d021789a6999a0cb782120276b482167f4c6954902558f13a099f762c65a5c43c2b68b412991fdbc462b427f688e5315c41647c90bbf1f422f01b3b609ec70c\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-api/2.3.5\",\n    \"dest-filename\": \"symbol-processing-api-2.3.5.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-api/2.3.5/symbol-processing-api-2.3.5.pom\",\n    \"sha512\": \"add5ae3eb926f825ab2897e3e1cc14bbc71ea5907dc73c3701c189a2cd7b4c9afc8f8d9afb88cd6346122975841cd7a5c502042c4ef2f509fc0212b85615f3f8\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-api/2.3.5\",\n    \"dest-filename\": \"symbol-processing-api-2.3.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-common-deps/2.3.5/symbol-processing-common-deps-2.3.5.jar\",\n    \"sha512\": \"d689128336ee69d8361b4c7532c9b6944d99a12ff20c78b86f8270ce9a5973196b1bddab93b8c5ed89dc92211a800f5827adaa3435b8ea1d11b67977a59d4114\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-common-deps/2.3.5\",\n    \"dest-filename\": \"symbol-processing-common-deps-2.3.5.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-common-deps/2.3.5/symbol-processing-common-deps-2.3.5.module\",\n    \"sha512\": \"78b8df83649dfa077485476cefcc36931e91c4dc61c7047d0918421525cd9e1adabf14cfaf5d17a2cb8c9239e72b835525784e2d142bc8a037fb3568ad465103\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-common-deps/2.3.5\",\n    \"dest-filename\": \"symbol-processing-common-deps-2.3.5.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-common-deps/2.3.5/symbol-processing-common-deps-2.3.5.pom\",\n    \"sha512\": \"c05835ab63fd7d34c3cf3066ebfa7da5b5887e35e607dd8ee4d6fb395860bef8bacdadd2ca15c684f769a3da4fdf245b7a55a1943591235b44b7fbfc44f03f1c\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-common-deps/2.3.5\",\n    \"dest-filename\": \"symbol-processing-common-deps-2.3.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-gradle-plugin/2.3.5/symbol-processing-gradle-plugin-2.3.5.jar\",\n    \"sha512\": \"e86a80253588a025e6e297e5a5f3c5ee32ea567b99ea997d11e7953f87048fa63edaa950b0f192e3ea2450c14973672fd0ee32779e022308afdf4433a94abeff\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-gradle-plugin/2.3.5\",\n    \"dest-filename\": \"symbol-processing-gradle-plugin-2.3.5.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-gradle-plugin/2.3.5/symbol-processing-gradle-plugin-2.3.5.module\",\n    \"sha512\": \"8b202cdf60d1bbf8b22afac91e1f8987ec2337753e46941b6c8ca0019e56e5c34d3be2baeb70f93ac7c816f7e5f43defb443d2747fdc29f0751e3223c98b38d0\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-gradle-plugin/2.3.5\",\n    \"dest-filename\": \"symbol-processing-gradle-plugin-2.3.5.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/devtools/ksp/symbol-processing-gradle-plugin/2.3.5/symbol-processing-gradle-plugin-2.3.5.pom\",\n    \"sha512\": \"b9c4d3211cf76694e3a3e5f44b9ee13fbf9481d8ff26f0a72036a031a4bd9dce5d6e760387a9ca7bce5f58e9dd5d8cb55dcc0d2701e402e8102d432d54a058c8\",\n    \"dest\": \"offline-repository/com/google/devtools/ksp/symbol-processing-gradle-plugin/2.3.5\",\n    \"dest-filename\": \"symbol-processing-gradle-plugin-2.3.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.41.0/error_prone_annotations-2.41.0.jar\",\n    \"sha512\": \"e2eb4bf9f36f95a4d4c5ea344db5cd90a456e63bef8e52932b8f6f4ecfdd59cb2f6c2ce9e67b0070c82177e42885688b95afef591b16001f789b378f18afdf30\",\n    \"dest\": \"offline-repository/com/google/errorprone/error_prone_annotations/2.41.0\",\n    \"dest-filename\": \"error_prone_annotations-2.41.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.41.0/error_prone_annotations-2.41.0.pom\",\n    \"sha512\": \"07bb0fc1b8659cd8c9ffb49563d4ec5f86bb30f5515eb6951e272de83e64670b0490f2e40b33ff6673f0d603719430c3378630478686d5849242028285204cdc\",\n    \"dest\": \"offline-repository/com/google/errorprone/error_prone_annotations/2.41.0\",\n    \"dest-filename\": \"error_prone_annotations-2.41.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_parent/2.41.0/error_prone_parent-2.41.0.pom\",\n    \"sha512\": \"ab85819cbab15554b3dbc1fd75eac6a13440178ee8c92a2b0fceeecac970f464d3aafc017842a2f7a692d15e7875389eb31ae9c0e08844990d7104f6c2f23f2a\",\n    \"dest\": \"offline-repository/com/google/errorprone/error_prone_parent/2.41.0\",\n    \"dest-filename\": \"error_prone_parent-2.41.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/flatbuffers/flatbuffers-java/1.12.0/flatbuffers-java-1.12.0.jar\",\n    \"sha512\": \"20750e91441c074ca28f72b0b6494e8f0bfbc94caff04eb49727efc6b28e453c84732f24acc52cf36b36ba66b57a145d003e87f749c5c4518ddc6f9247b8ab47\",\n    \"dest\": \"offline-repository/com/google/flatbuffers/flatbuffers-java/1.12.0\",\n    \"dest-filename\": \"flatbuffers-java-1.12.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/flatbuffers/flatbuffers-java/1.12.0/flatbuffers-java-1.12.0.pom\",\n    \"sha512\": \"57d6a98dbd649ad67f3a26fc5a473cdeabd8e0dbec03ce0d95e97d302c074399690ad96442fa412ab5780e9a1d612ea07e441fff3d958e265daea2aba2acfa33\",\n    \"dest\": \"offline-repository/com/google/flatbuffers/flatbuffers-java/1.12.0\",\n    \"dest-filename\": \"flatbuffers-java-1.12.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar\",\n    \"sha512\": \"ff4ee76aa661708989d53d45576cff3beea9ebbd86481dbbf2ee8c81bb22f882097b430588312b711025f0e890f22c6799d722ccd422a6a7278de08660fe2f51\",\n    \"dest\": \"offline-repository/com/google/guava/failureaccess/1.0.2\",\n    \"dest-filename\": \"failureaccess-1.0.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.pom\",\n    \"sha512\": \"4e5144a31143d0ee374dc323752d57c28d7a0117abcf75a67397ba1a26c93dcf2c248c357d52c4ce75e2fe7c366df909a0a77db5775cc30c92c4e72a433566af\",\n    \"dest\": \"offline-repository/com/google/guava/failureaccess/1.0.2\",\n    \"dest-filename\": \"failureaccess-1.0.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/26.0-android/guava-parent-26.0-android.pom\",\n    \"sha512\": \"1d786f14fbfa5c90eedcc160d1e0a71acb2141f372049b22ce62b0bd1e883c17cc24a59dc8b00e5037e959cccdb54d4d8dc8f252302d4bb7ce82dfdaff764476\",\n    \"dest\": \"offline-repository/com/google/guava/guava-parent/26.0-android\",\n    \"dest-filename\": \"guava-parent-26.0-android.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/guava-parent/33.3.1-jre/guava-parent-33.3.1-jre.pom\",\n    \"sha512\": \"3927dd059c1f983b7a390b08f93f49931231a20a5c77b6b94eca6e536df1a5c1d8f2248ed9e906e3fc621f2c44421c5dee9e82f8a722d9cea1e35a86cfc28a66\",\n    \"dest\": \"offline-repository/com/google/guava/guava-parent/33.3.1-jre\",\n    \"dest-filename\": \"guava-parent-33.3.1-jre.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar\",\n    \"sha512\": \"5fdb406eeafbb7b409bae82924b7c26bdcdea06c7edbb673901814c3d35010bc91139e5139663af0b66f8fa47f54e0e96dd86fdff1797c9ae497c10a466b02e3\",\n    \"dest\": \"offline-repository/com/google/guava/guava/33.3.1-jre\",\n    \"dest-filename\": \"guava-33.3.1-jre.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.module\",\n    \"sha512\": \"9ba76c1e924406aefe5e3a37e536fa4301a038fc671605eb7d2ffde7723e25fdbe6e26b6a297d5fe74cee8790fc0776789865254a30a1c87c7c00c499a2c4aae\",\n    \"dest\": \"offline-repository/com/google/guava/guava/33.3.1-jre\",\n    \"dest-filename\": \"guava-33.3.1-jre.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.pom\",\n    \"sha512\": \"edc26505ae9ec67ea167996de4ca09a095bd341d19a004241332c59992dca53e21c07814d0dd5f33ba7934659e7ae1ac85ec74ef7da248074dfe35aa55ba14e8\",\n    \"dest\": \"offline-repository/com/google/guava/guava/33.3.1-jre\",\n    \"dest-filename\": \"guava-33.3.1-jre.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\",\n    \"sha512\": \"c5987a979174cbacae2e78b319f080420cc71bcdbcf7893745731eeb93c23ed13bff8d4599441f373f3a246023d33df03e882de3015ee932a74a774afdd0782f\",\n    \"dest\": \"offline-repository/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava\",\n    \"dest-filename\": \"listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.pom\",\n    \"sha512\": \"140544970539199c860d5e7e2d8096bbccd55980602773f29e8d74ab4f3e24c43aa369b0c7d06b026ba7a9db3353be285d7ffb9cd94b2bedf62820c3fdfd1d5f\",\n    \"dest\": \"offline-repository/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava\",\n    \"dest-filename\": \"listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/3.0.0/j2objc-annotations-3.0.0.jar\",\n    \"sha512\": \"1406b1aa53b19f8269129d96ce8b64bf36f215eacf7d8f1e0adadee31614e53bb3f7acf4ff97418c5bfc75677a6f3cd637c3d9889d1e85117b6fa12467c91e9f\",\n    \"dest\": \"offline-repository/com/google/j2objc/j2objc-annotations/3.0.0\",\n    \"dest-filename\": \"j2objc-annotations-3.0.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/3.0.0/j2objc-annotations-3.0.0.pom\",\n    \"sha512\": \"3c0f19b8e1275dbbaf8a0d02f9ef27a7edbb73ffdd75ae2c9fd334b5dbc373e8420f896b962c86f5d3540007c07acd1995eed8d119d7c6e7068305e9c12f0181\",\n    \"dest\": \"offline-repository/com/google/j2objc/j2objc-annotations/3.0.0\",\n    \"dest-filename\": \"j2objc-annotations-3.0.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs-parent/1.1/jimfs-parent-1.1.pom\",\n    \"sha512\": \"83091a57b7436eef0f1452405358ce87d85813bb59275ff9b51b5b6f23b10627138e5077f4611ffc828845445107607e08648cb28dce50b8b1746c09329783d9\",\n    \"dest\": \"offline-repository/com/google/jimfs/jimfs-parent/1.1\",\n    \"dest-filename\": \"jimfs-parent-1.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs/1.1/jimfs-1.1.jar\",\n    \"sha512\": \"a915da137c45e2ce1aca552b3658545a50c893c9dc971a1992d1e05b9c7901ee22d5b19f9489353ed4de149a8a72d150e1605ad164e52ce4ff97753969794751\",\n    \"dest\": \"offline-repository/com/google/jimfs/jimfs/1.1\",\n    \"dest-filename\": \"jimfs-1.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/jimfs/jimfs/1.1/jimfs-1.1.pom\",\n    \"sha512\": \"af3cabfa14bac0d46470416b4a2747404cf6ec9da96688ede925131ffba3da6a74f1b73db4aa6a44ea11f90ded4f308aa4e8bd16c9581b9a7ed95d79aa330384\",\n    \"dest\": \"offline-repository/com/google/jimfs/jimfs/1.1\",\n    \"dest-filename\": \"jimfs-1.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-bom/3.25.5/protobuf-bom-3.25.5.pom\",\n    \"sha512\": \"b900a81cfd33a1e85c903ef0eeea375a1e5d85cc6ccb3470b7bbbcceaf928e9e8670405a546aafe2cd0e7e9ed565628eb0990bcb1ae643e0c394f2745cd84007\",\n    \"dest\": \"offline-repository/com/google/protobuf/protobuf-bom/3.25.5\",\n    \"dest-filename\": \"protobuf-bom-3.25.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.25.5/protobuf-java-util-3.25.5.jar\",\n    \"sha512\": \"bd9411eba3911839ded3dc26536e3383f3135738ec0f17ecd29681f99cbc8dd50dfce73b7da9ab80eca6adea5271cff337192e7d96fea17fe27ee92e69a4d07f\",\n    \"dest\": \"offline-repository/com/google/protobuf/protobuf-java-util/3.25.5\",\n    \"dest-filename\": \"protobuf-java-util-3.25.5.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java-util/3.25.5/protobuf-java-util-3.25.5.pom\",\n    \"sha512\": \"7af1dda607c8d3102670c2dc2d130a5a38dc3da4c54d8111e63137fcba924e206333baedb14cdef5a37cf5dbff0e99db92951419837ae676d0ebbdbcc9400acc\",\n    \"dest\": \"offline-repository/com/google/protobuf/protobuf-java-util/3.25.5\",\n    \"dest-filename\": \"protobuf-java-util-3.25.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.25.5/protobuf-java-3.25.5.jar\",\n    \"sha512\": \"432d8a9359e614d38fe416b7a4564aed3e358fd5f3c2c4f22caf97945a0f3e5cbd2220b690d6b822504e7bcbbd67458eb12d232232dd113f804683a172f8eb71\",\n    \"dest\": \"offline-repository/com/google/protobuf/protobuf-java/3.25.5\",\n    \"dest-filename\": \"protobuf-java-3.25.5.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.25.5/protobuf-java-3.25.5.pom\",\n    \"sha512\": \"2e9ea8539bf92bf9ab89e08614fdccd4a663db1f2cf5b4e35f59ec7276a34d9e83e57da51bcd7ef5959016072768a9ba105f78d6c1334dbc9e61f90b37103810\",\n    \"dest\": \"offline-repository/com/google/protobuf/protobuf-java/3.25.5\",\n    \"dest-filename\": \"protobuf-java-3.25.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-parent/3.25.5/protobuf-parent-3.25.5.pom\",\n    \"sha512\": \"7470211ceeff2ee79ac892d9f1ed22e698a0f9e07ac1546943bd5abd20e0133de075ec00a5bb230d16ffe900995ef028ff950c7395887ba3108c59ecf1074456\",\n    \"dest\": \"offline-repository/com/google/protobuf/protobuf-parent/3.25.5\",\n    \"dest-filename\": \"protobuf-parent-3.25.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/google/testing/platform/core-proto/0.0.9-alpha03/core-proto-0.0.9-alpha03.jar\",\n    \"sha512\": \"39f974779b3a8ab5359c0f499cabd0dd021bdf67e1c5fddf4c2838589e9984b812f52eb095d8b6da749a98ac29400a873ca7f6ffef5063b76fc181a75e129b26\",\n    \"dest\": \"offline-repository/com/google/testing/platform/core-proto/0.0.9-alpha03\",\n    \"dest-filename\": \"core-proto-0.0.9-alpha03.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://dl.google.com/dl/android/maven2/com/google/testing/platform/core-proto/0.0.9-alpha03/core-proto-0.0.9-alpha03.pom\",\n    \"sha512\": \"55d689847fcb5c4cfe724232491948f26f7790aaabfbf5a2d72e5ad093b8310ba956bd5830b004592fd24df08e6af629f7746d3ce427635e51e5577ee5b7214a\",\n    \"dest\": \"offline-repository/com/google/testing/platform/core-proto/0.0.9-alpha03\",\n    \"dest-filename\": \"core-proto-0.0.9-alpha03.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/googlecode/juniversalchardet/juniversalchardet/1.0.3/juniversalchardet-1.0.3.jar\",\n    \"sha512\": \"62278bb82d1dcbf64e92e5314ff1ce481e34e3d4bcc2aad54d410b339f00b33ceb8aaddf6ed86e839d924f17d25948a0ae4a37015724076048d14e1aa66c942d\",\n    \"dest\": \"offline-repository/com/googlecode/juniversalchardet/juniversalchardet/1.0.3\",\n    \"dest-filename\": \"juniversalchardet-1.0.3.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/googlecode/juniversalchardet/juniversalchardet/1.0.3/juniversalchardet-1.0.3.pom\",\n    \"sha512\": \"0e38ec949810af17ed539dd65159cdf5d605e1b0491e95cd227585d77c0dceca4b41ee642345cdc7b004b4be08d26f15e9b91d0aaf12a654a341b835a9d8d002\",\n    \"dest\": \"offline-repository/com/googlecode/juniversalchardet/juniversalchardet/1.0.3\",\n    \"dest-filename\": \"juniversalchardet-1.0.3.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/squareup/javapoet/1.10.0/javapoet-1.10.0.jar\",\n    \"sha512\": \"67fe213b5ddd71fca4e28758025b34feb82fe7b5e5f50aa9c652714c21fddf0bf6fbd0c21c555e6824f5ff989a1feefc448a0cb376b2fa7c684abff70d51769c\",\n    \"dest\": \"offline-repository/com/squareup/javapoet/1.10.0\",\n    \"dest-filename\": \"javapoet-1.10.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/squareup/javapoet/1.10.0/javapoet-1.10.0.pom\",\n    \"sha512\": \"de661d83bd261fa527a5c8447dc14621a638aba12c697f59a393f47175f01235532ec8df5067d5c155351256fb3b07eed28e0c097e263052709e276224cafe81\",\n    \"dest\": \"offline-repository/com/squareup/javapoet/1.10.0\",\n    \"dest-filename\": \"javapoet-1.10.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/squareup/javawriter/2.5.0/javawriter-2.5.0.jar\",\n    \"sha512\": \"0d1df927a41b762b30bdc520ae6ea465ded1385a2b0a0d91dfcac8c0d87388089d8a881361060d1df0fe833944864fc23f9fe38c30d9ce106cc3e846f072dcf3\",\n    \"dest\": \"offline-repository/com/squareup/javawriter/2.5.0\",\n    \"dest-filename\": \"javawriter-2.5.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/squareup/javawriter/2.5.0/javawriter-2.5.0.pom\",\n    \"sha512\": \"81d5507e44a1ba76c61566abfa1d189f2136960ba638c247494f8c55bd99ac34ea64e88e5d3c8d5ace51c856d36bb941c90bc1170346d8e278d8b5c0006f2954\",\n    \"dest\": \"offline-repository/com/squareup/javawriter/2.5.0\",\n    \"dest-filename\": \"javawriter-2.5.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/activation/all/1.2.0/all-1.2.0.pom\",\n    \"sha512\": \"9efe921f24ff6f0e5713f3e327b0029ddaba7bf7da93738ca04f80ccad2f0a26870195ba7eb990d8a026daedd83daf8042137cce9cd71ffeef12ddc329e3163d\",\n    \"dest\": \"offline-repository/com/sun/activation/all/1.2.0\",\n    \"dest-filename\": \"all-1.2.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/activation/all/1.2.1/all-1.2.1.pom\",\n    \"sha512\": \"87de9589f96f717f3b2d665aad8eb531ed677aa0b34528c049c9edb964f20b275dda112d24eb7c630ccc1ccf6cead40147f567391a2e99165a0d33b068473beb\",\n    \"dest\": \"offline-repository/com/sun/activation/all/1.2.1\",\n    \"dest-filename\": \"all-1.2.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/activation/javax.activation/1.2.0/javax.activation-1.2.0.jar\",\n    \"sha512\": \"b4cbdd8fd1703e4b2e1e691db78fbcf2232d836f740d1821c4c191a14f9472508e27a40d06e4b6b153964af68032959c22945ba169a0ca4018b7748162f420a6\",\n    \"dest\": \"offline-repository/com/sun/activation/javax.activation/1.2.0\",\n    \"dest-filename\": \"javax.activation-1.2.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/activation/javax.activation/1.2.0/javax.activation-1.2.0.pom\",\n    \"sha512\": \"a90ba81c3572f2169933f535b10a5b9cf6405e8b812464887ae2b5a2b25532be2a905468796512bd034119883869825b3ac858d51da23d01afab6bb990577ade\",\n    \"dest\": \"offline-repository/com/sun/activation/javax.activation/1.2.0\",\n    \"dest-filename\": \"javax.activation-1.2.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons-runtime/3.0.8/istack-commons-runtime-3.0.8.jar\",\n    \"sha512\": \"e712166f961cca819a61bd6efc93b93759788d283af73dcc6ed09c0f9c045d7dc2f7fabc0e236b2996fcdebe26e1b2ec5ea45eb4353943dad82ecfef0488305e\",\n    \"dest\": \"offline-repository/com/sun/istack/istack-commons-runtime/3.0.8\",\n    \"dest-filename\": \"istack-commons-runtime-3.0.8.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons-runtime/3.0.8/istack-commons-runtime-3.0.8.pom\",\n    \"sha512\": \"d365eced43f3d5743ad798f14ef7715cec40b4b107dd56142fa61b5db5a52b454d312904d6763a8221f47008290e47c5f1546b1e67f0b0f5075734416ad63992\",\n    \"dest\": \"offline-repository/com/sun/istack/istack-commons-runtime/3.0.8\",\n    \"dest-filename\": \"istack-commons-runtime-3.0.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/istack/istack-commons/3.0.8/istack-commons-3.0.8.pom\",\n    \"sha512\": \"d032138423fddfeaffe303b01a85a4d68766f1af0af43a8a24d122d665b2016c110b10ce77fcff1851ccfaaabb46e0f3faf6c664c05efd271705829ef8466f06\",\n    \"dest\": \"offline-repository/com/sun/istack/istack-commons/3.0.8\",\n    \"dest-filename\": \"istack-commons-3.0.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/bind/jaxb-bom-ext/2.3.2/jaxb-bom-ext-2.3.2.pom\",\n    \"sha512\": \"2445bdddf6f353ee89f81b4a1b392c73f72f867a3a68a611f98e4bcdd608b49aa65dc0f1bacd65c0b95f87dc1d6c02f6c066b6f3b7a5ffb7270bfdc7ee6d482d\",\n    \"dest\": \"offline-repository/com/sun/xml/bind/jaxb-bom-ext/2.3.2\",\n    \"dest-filename\": \"jaxb-bom-ext-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-parent/2.3.2/jaxb-parent-2.3.2.pom\",\n    \"sha512\": \"0464d8a6bea1e3f5e5c5f1e1dd30fbf637a966395421c796a3fc09ce6d9da74a8a0d810ad013c25b946627e663576b6b1d99a1a16048ba0d29dd23935a2d4963\",\n    \"dest\": \"offline-repository/com/sun/xml/bind/mvn/jaxb-parent/2.3.2\",\n    \"dest-filename\": \"jaxb-parent-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.2/jaxb-runtime-parent-2.3.2.pom\",\n    \"sha512\": \"6916a297ce970d4e2029ff79a4d4fd6f056520644e64922175194f7810c67c1914ed66e2a6da34c1e518d18a1636ad91fd2d328afd502d55d0f2366b0ccc2020\",\n    \"dest\": \"offline-repository/com/sun/xml/bind/mvn/jaxb-runtime-parent/2.3.2\",\n    \"dest-filename\": \"jaxb-runtime-parent-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/bind/mvn/jaxb-txw-parent/2.3.2/jaxb-txw-parent-2.3.2.pom\",\n    \"sha512\": \"d26ae2bf5e22343350de9c544a13598f404ab659ad88863dbe1f5423a1b96563ee56c0620e71ac0768a921a47ed211256a0d3a4bb9973894f0cac484bcfa0c0c\",\n    \"dest\": \"offline-repository/com/sun/xml/bind/mvn/jaxb-txw-parent/2.3.2\",\n    \"dest-filename\": \"jaxb-txw-parent-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/FastInfoset/1.2.16/FastInfoset-1.2.16.jar\",\n    \"sha512\": \"cd57377d61d66ac2ab6ab90483252385fe2bbb9e13dee07913362a64ffe97b16082df981dbe469df3f67ad946827be6f4355d63003a393bda62a0fc8b54a590b\",\n    \"dest\": \"offline-repository/com/sun/xml/fastinfoset/FastInfoset/1.2.16\",\n    \"dest-filename\": \"FastInfoset-1.2.16.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/FastInfoset/1.2.16/FastInfoset-1.2.16.pom\",\n    \"sha512\": \"8c2c6b0d37838ea9a27329af479b0c70cff3b93f78828c3d9a959db99a8c639b2b00fe382e3df14ee7bc4cc63c4cb98ab5e827fc8921b39eb8afce02944669dc\",\n    \"dest\": \"offline-repository/com/sun/xml/fastinfoset/FastInfoset/1.2.16\",\n    \"dest-filename\": \"FastInfoset-1.2.16.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/com/sun/xml/fastinfoset/fastinfoset-project/1.2.16/fastinfoset-project-1.2.16.pom\",\n    \"sha512\": \"07b2ceef7b3062f865fcf88636056bc5b5d071f05c49fa74e8b01ecdf5030ce5845bc065de4c6f479f2ea1e400dd5b45690b7dbb0516fa6460c64b2745fd1af0\",\n    \"dest\": \"offline-repository/com/sun/xml/fastinfoset/fastinfoset-project/1.2.16\",\n    \"dest-filename\": \"fastinfoset-project-1.2.16.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.11/commons-codec-1.11.jar\",\n    \"sha512\": \"d9586162b257386b5871e7e9ae255a38014a9efaeef5148de5e40a3b0200364dad8516bddd554352aa2e5337bec2cc11df88c76c4fdde96a40f3421aa60650d7\",\n    \"dest\": \"offline-repository/commons-codec/commons-codec/1.11\",\n    \"dest-filename\": \"commons-codec-1.11.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.11/commons-codec-1.11.pom\",\n    \"sha512\": \"1b7a0a986a1bd222c0795972daabee4f16e308498f67d62b99749506b5a71e9811deaa323c080517a8184d6481e8281c91bd012d5614b7d5d2ba14aa96f2596c\",\n    \"dest\": \"offline-repository/commons-codec/commons-codec/1.11\",\n    \"dest-filename\": \"commons-codec-1.11.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/commons-io/commons-io/2.16.1/commons-io-2.16.1.jar\",\n    \"sha512\": \"97eab31b073c5c57c8bcfaa2fec7b481a15a9a1f9ed864dfdc63b57f062b230557caa734c3133aca1165facb588c58db0185c07832241d70159e87a4bcf48008\",\n    \"dest\": \"offline-repository/commons-io/commons-io/2.16.1\",\n    \"dest-filename\": \"commons-io-2.16.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/commons-io/commons-io/2.16.1/commons-io-2.16.1.pom\",\n    \"sha512\": \"c7c29f4d44cc8dc1b60c2e29dee585a7eb4941fb7664140c0a22832056bba7cf79ed616911a72703090e72e4a223f24439e5021c3defa37d64e5d533b67d7ee2\",\n    \"dest\": \"offline-repository/commons-io/commons-io/2.16.1\",\n    \"dest-filename\": \"commons-io-2.16.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/commons-logging/commons-logging/1.2/commons-logging-1.2.jar\",\n    \"sha512\": \"ed00dbfabd9ae00efa26dd400983601d076fe36408b7d6520084b447e5d1fa527ce65bd6afdcb58506c3a808323d28e88f26cb99c6f5db9ff64f6525ecdfa557\",\n    \"dest\": \"offline-repository/commons-logging/commons-logging/1.2\",\n    \"dest-filename\": \"commons-logging-1.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/commons-logging/commons-logging/1.2/commons-logging-1.2.pom\",\n    \"sha512\": \"75bef548eea62ab04569791f2fdeed3d0a61edae0534aa035a905dc1d011988fc0f06f52bde377f44e94e6afd4380197148120b152b7a4d20628fb6236cc7261\",\n    \"dest\": \"offline-repository/commons-logging/commons-logging/1.2\",\n    \"dest-filename\": \"commons-logging-1.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://plugins.gradle.org/m2/io/github/jwharm/flatpak-gradle-generator/io.github.jwharm.flatpak-gradle-generator.gradle.plugin/1.7.0/io.github.jwharm.flatpak-gradle-generator.gradle.plugin-1.7.0.pom\",\n    \"sha512\": \"fba54b6f68e32382288201b0a360a6ba34f5a24bfc2f6ced503384f1bf67de2ca5c4f417a91616c2da591d185202d0c58acee2ea3a1ed2a8810011cdc1fe5056\",\n    \"dest\": \"offline-repository/io/github/jwharm/flatpak-gradle-generator/io.github.jwharm.flatpak-gradle-generator.gradle.plugin/1.7.0\",\n    \"dest-filename\": \"io.github.jwharm.flatpak-gradle-generator.gradle.plugin-1.7.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://plugins.gradle.org/m2/io/github/jwharm/flatpak-gradle-generator/plugin/1.7.0/plugin-1.7.0.jar\",\n    \"sha512\": \"1fd33596f12f7f2e30e0a59db804cc1a65326b711f5bc8eafccaaf6bee495a33971354d58b17173f5a3f8053f2240e34146ca8e02f230b3319f2d22f5225b8fb\",\n    \"dest\": \"offline-repository/io/github/jwharm/flatpak-gradle-generator/plugin/1.7.0\",\n    \"dest-filename\": \"plugin-1.7.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://plugins.gradle.org/m2/io/github/jwharm/flatpak-gradle-generator/plugin/1.7.0/plugin-1.7.0.module\",\n    \"sha512\": \"4b0865f472199aa42925b39bed7e211e67ca935c4a5e6598d46cc5da313e91a2de6f6d0c775c06b6c79e01d46ab4f44d67ad4d1e5201efd9086d1cccba8e8bad\",\n    \"dest\": \"offline-repository/io/github/jwharm/flatpak-gradle-generator/plugin/1.7.0\",\n    \"dest-filename\": \"plugin-1.7.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://plugins.gradle.org/m2/io/github/jwharm/flatpak-gradle-generator/plugin/1.7.0/plugin-1.7.0.pom\",\n    \"sha512\": \"ecc20ed5e412a877d83ecf394e55376492535831d3237f348566bed8fcd31154641e74e4a4f5e841171069be83ef8f53e18dcddaffef3c8847b8ae2a75dd0a49\",\n    \"dest\": \"offline-repository/io/github/jwharm/flatpak-gradle-generator/plugin/1.7.0\",\n    \"dest-filename\": \"plugin-1.7.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-api/1.69.1/grpc-api-1.69.1.jar\",\n    \"sha512\": \"e3cfa77db26b4fc45295b06a2b7636244cef02abd18f5131136c73fe276ea57acddd03fe426fe943d2e6cbb075a102aeed54ae411a6199c16ddeb7e171d0866e\",\n    \"dest\": \"offline-repository/io/grpc/grpc-api/1.69.1\",\n    \"dest-filename\": \"grpc-api-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-api/1.69.1/grpc-api-1.69.1.pom\",\n    \"sha512\": \"cc5f8e93d88372cb87d4a5aca1429df0e87986ac86fad758d4ef73d67cf883e9046ae429e8a398bc18cf6c9f27996aec066d7a80579ed6d2b392bfe157ccd1ab\",\n    \"dest\": \"offline-repository/io/grpc/grpc-api/1.69.1\",\n    \"dest-filename\": \"grpc-api-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-context/1.69.1/grpc-context-1.69.1.jar\",\n    \"sha512\": \"72dde2a9a83aaccd73b92931d80916c222578ac710f597a967f4041ebc4d1a6d6979cf773a9404729ca8f200672b77d76523c523f2ed50e0f39f5f1e15e3a74f\",\n    \"dest\": \"offline-repository/io/grpc/grpc-context/1.69.1\",\n    \"dest-filename\": \"grpc-context-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-context/1.69.1/grpc-context-1.69.1.pom\",\n    \"sha512\": \"f0a2b953383088d3c7b4a8b1e89cb29e9551608a742b78c6c2de8751fa292d5c050e7277e08222a2a7735d31b282bf58cfc06778c87eca10ee8ba2a186491efe\",\n    \"dest\": \"offline-repository/io/grpc/grpc-context/1.69.1\",\n    \"dest-filename\": \"grpc-context-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-core/1.69.1/grpc-core-1.69.1.jar\",\n    \"sha512\": \"e9fcf546decd203e536848083dcabd60cf0c930e3bb63245e615d2b30fc72aeb8ace9676df01f9ad9c88a1b40098021df4ce6f26e64658ec55fdf1dce671aa6b\",\n    \"dest\": \"offline-repository/io/grpc/grpc-core/1.69.1\",\n    \"dest-filename\": \"grpc-core-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-core/1.69.1/grpc-core-1.69.1.pom\",\n    \"sha512\": \"921ddda33077e5310162f33b2864a90438c77ecf33f4d0ac5ac9f2d75ca7e8b1f29c5fa60f8b27944977bc27e2000dfc3f74c57174884d9c18329e99f51f80f4\",\n    \"dest\": \"offline-repository/io/grpc/grpc-core/1.69.1\",\n    \"dest-filename\": \"grpc-core-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-inprocess/1.69.1/grpc-inprocess-1.69.1.jar\",\n    \"sha512\": \"dd625d8132229b7dbc677057921017ce2578bf11aff60201f9be4435dc78459651fa8ffd1d7688ab876f1742b521b73acd83ecb35d4b23c3ca68c10a2e37d2c0\",\n    \"dest\": \"offline-repository/io/grpc/grpc-inprocess/1.69.1\",\n    \"dest-filename\": \"grpc-inprocess-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-inprocess/1.69.1/grpc-inprocess-1.69.1.pom\",\n    \"sha512\": \"d5f1ff28a9238a3921a0163c33131fb1aabd1b6f81c027042d21fbdf24bf5c3c53de9420c609b5311e931290acd6f81880eaa82a9ce83d4f10ade94ddd242d8d\",\n    \"dest\": \"offline-repository/io/grpc/grpc-inprocess/1.69.1\",\n    \"dest-filename\": \"grpc-inprocess-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-netty/1.69.1/grpc-netty-1.69.1.jar\",\n    \"sha512\": \"bd87abb67c5a6ecc36625e7d3cd2e3a68e115846464fe43801a7dcd65c0040c2372863e743bf19fa8a112731ebaf1450aedc11050a36fc41b55d22dc0d4b503c\",\n    \"dest\": \"offline-repository/io/grpc/grpc-netty/1.69.1\",\n    \"dest-filename\": \"grpc-netty-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-netty/1.69.1/grpc-netty-1.69.1.pom\",\n    \"sha512\": \"30e46fa896078aa1da5d9874d210da42b50140c6f701b4850c06166bcc8326fb2ae1ea9e303191c542b96b1e93bbf9ab7af0d2baed8a22fdfc10e6b55c1e2704\",\n    \"dest\": \"offline-repository/io/grpc/grpc-netty/1.69.1\",\n    \"dest-filename\": \"grpc-netty-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf-lite/1.69.1/grpc-protobuf-lite-1.69.1.jar\",\n    \"sha512\": \"64a10975dc589221f207cea8d31893b2e7318db91fe4f391b66521c004747c47e72b96e2485ff0d293d28465a09316481efe364f6186b99edb7729d7a34fcb07\",\n    \"dest\": \"offline-repository/io/grpc/grpc-protobuf-lite/1.69.1\",\n    \"dest-filename\": \"grpc-protobuf-lite-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf-lite/1.69.1/grpc-protobuf-lite-1.69.1.pom\",\n    \"sha512\": \"fca5aeacd598ac36eaeea0a46ecc67bde5feccd7389d15f32f7c573dc008d102d19932e6c9a1c275154b8928b27ada4299d27bfc0445af3667ed682da470cd99\",\n    \"dest\": \"offline-repository/io/grpc/grpc-protobuf-lite/1.69.1\",\n    \"dest-filename\": \"grpc-protobuf-lite-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf/1.69.1/grpc-protobuf-1.69.1.jar\",\n    \"sha512\": \"6d81e633179b5acc71229802a0de0f7072cf5639281305d39b7b7409f1945be3f7f93cd014b6a62b9ab77d1d9610f242a8bcda3499494743913ac79bb42e43d1\",\n    \"dest\": \"offline-repository/io/grpc/grpc-protobuf/1.69.1\",\n    \"dest-filename\": \"grpc-protobuf-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-protobuf/1.69.1/grpc-protobuf-1.69.1.pom\",\n    \"sha512\": \"04b7e993b0a75427bf6dbbe64952aff9694f0300fdceb130297b2a52f111c56475390f887916454a4833abbec4809f5c6b0c5ba6117df6d1ee301f0e0db5afe0\",\n    \"dest\": \"offline-repository/io/grpc/grpc-protobuf/1.69.1\",\n    \"dest-filename\": \"grpc-protobuf-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-stub/1.69.1/grpc-stub-1.69.1.jar\",\n    \"sha512\": \"6c62369eec7d9f89e18554c6de5993d6db31f44d44df6f6ab88085fc7fbebe415e06402726ec73f628a992145ea8336df93daa1df434b2d5bcb2f25ea9d94486\",\n    \"dest\": \"offline-repository/io/grpc/grpc-stub/1.69.1\",\n    \"dest-filename\": \"grpc-stub-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-stub/1.69.1/grpc-stub-1.69.1.pom\",\n    \"sha512\": \"77ebb20dd3433aaed7387cee2ad646c2652f1acc8a43010dd7bd0a3c7cf2283ba97032179fc7740ba5ddddb5c9b464b1638351fd31b4c74e870a0bde6a88f755\",\n    \"dest\": \"offline-repository/io/grpc/grpc-stub/1.69.1\",\n    \"dest-filename\": \"grpc-stub-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-util/1.69.1/grpc-util-1.69.1.jar\",\n    \"sha512\": \"1fb57e62b0f61d583acdf524279df65ca8f25f774384926f5b5b11e5796d82852e30fea5869c973e0f1617957aecf17fef18c9ce0ece547c47bd19e3a6595341\",\n    \"dest\": \"offline-repository/io/grpc/grpc-util/1.69.1\",\n    \"dest-filename\": \"grpc-util-1.69.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/grpc/grpc-util/1.69.1/grpc-util-1.69.1.pom\",\n    \"sha512\": \"726d9f8439fb2296ab05e086e267a9c38e8c18bea8fcc55b20961e6edc5a660aafcf446c3fd7ab2bc92af34febcb55f1ae3c880b1b64ad9c1f3bce7a932aa5ac\",\n    \"dest\": \"offline-repository/io/grpc/grpc-util/1.69.1\",\n    \"dest-filename\": \"grpc-util-1.69.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-buffer/4.1.110.Final/netty-buffer-4.1.110.Final.jar\",\n    \"sha512\": \"4c7c3fde964bf295cd31130e6b0e791f6791042cc526c83fcf2be5376de06c294fcbf49d3199907866e5e8ad27ff873deb66c294b16057135a161f43cf5e617e\",\n    \"dest\": \"offline-repository/io/netty/netty-buffer/4.1.110.Final\",\n    \"dest-filename\": \"netty-buffer-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-buffer/4.1.110.Final/netty-buffer-4.1.110.Final.pom\",\n    \"sha512\": \"e3982d621b82a82e2d87ba3dd781a7b062f2f447d1d651fa82252a7d1ff63421fe2468e0cfc7ce5930faf8d6f8401e986b550fdb3e6ec40a2fbee097388185e9\",\n    \"dest\": \"offline-repository/io/netty/netty-buffer/4.1.110.Final\",\n    \"dest-filename\": \"netty-buffer-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.110.Final/netty-codec-http-4.1.110.Final.jar\",\n    \"sha512\": \"df5f913a4dca7607aacc420ac67c0727abbd2b54b195876e53dfa90994c1c64b18df1cb08d74de716ed998e9b81de5f872fc68d75632676720ac527921b1a3c0\",\n    \"dest\": \"offline-repository/io/netty/netty-codec-http/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-http-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.110.Final/netty-codec-http-4.1.110.Final.pom\",\n    \"sha512\": \"6f8c1eb86999fe03758afe42a37bc0ab024ef7d7348f61c0fa56b4f4cf1f1d3342af7d0e37052095b0e352524be016d5e77dd770c2b298d33dadedce9bd729db\",\n    \"dest\": \"offline-repository/io/netty/netty-codec-http/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-http-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec-http2/4.1.110.Final/netty-codec-http2-4.1.110.Final.jar\",\n    \"sha512\": \"04b33db9e923d4a70675106aafcb3f2f840b403091c1864ad9f00a2325ff6eb47c19cf2143e64506bb750a4704e646bcb23181a849198f00a34347696a5a9978\",\n    \"dest\": \"offline-repository/io/netty/netty-codec-http2/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-http2-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec-http2/4.1.110.Final/netty-codec-http2-4.1.110.Final.pom\",\n    \"sha512\": \"259c577997e2b6b5a8cd8667b2f613b6eb81164aaab22647b01a98efbf8309cd53140382c5f5a7143661decabe8d0a01e6e4d10ad1f8b78d12aea0cce71c765a\",\n    \"dest\": \"offline-repository/io/netty/netty-codec-http2/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-http2-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec-socks/4.1.110.Final/netty-codec-socks-4.1.110.Final.jar\",\n    \"sha512\": \"3976983153b45d11f1843adfd58ddff70dd36591754207639786e83666809d6d97a1ec5b5729b80cbf2c7d1f3950be575eb63b9fc90ea6acb88b940ce99abb0a\",\n    \"dest\": \"offline-repository/io/netty/netty-codec-socks/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-socks-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec-socks/4.1.110.Final/netty-codec-socks-4.1.110.Final.pom\",\n    \"sha512\": \"2e9e341655542b901741e0555c14db6a44c78cba1ef317c751678c9a6d54db17a2beb6a41fdd8d274571a83cae7d8db141866ac59691138faaeb0286c95b58b0\",\n    \"dest\": \"offline-repository/io/netty/netty-codec-socks/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-socks-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec/4.1.110.Final/netty-codec-4.1.110.Final.jar\",\n    \"sha512\": \"4ee4546f1a14e13c9cfc9dd489023a4cd6d427431f5ee36884ad30f8e1ae7ab50fffdeef821db09998fd53d2a0985cdc8060de6e0296e166e23752381e09b763\",\n    \"dest\": \"offline-repository/io/netty/netty-codec/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-codec/4.1.110.Final/netty-codec-4.1.110.Final.pom\",\n    \"sha512\": \"7167051b8a065501408969eaa469c1fd6cee6b76281b03d49e2e8d99326952f6be3091c964f26482a3216dc022ff7f25bb362acc0e138104cb273fae0222b229\",\n    \"dest\": \"offline-repository/io/netty/netty-codec/4.1.110.Final\",\n    \"dest-filename\": \"netty-codec-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-common/4.1.110.Final/netty-common-4.1.110.Final.jar\",\n    \"sha512\": \"5c09ddc68f497a749e0d318e3cec216e18d1f380ad67fa70d55ce109ba552cd3cd02a4717535cef1567f8502dfe1a8f1772e0583f8bc526bea4cfc18fd5fe5dd\",\n    \"dest\": \"offline-repository/io/netty/netty-common/4.1.110.Final\",\n    \"dest-filename\": \"netty-common-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-common/4.1.110.Final/netty-common-4.1.110.Final.pom\",\n    \"sha512\": \"5be38b05f7704cb1096295ef78eef31a2628a7a0de300498e9a07b28fbcfb53597bbd2e7405dfe3cd81eff0684feab1113ce18c4813c53d28ae6f2b31921ccd4\",\n    \"dest\": \"offline-repository/io/netty/netty-common/4.1.110.Final\",\n    \"dest-filename\": \"netty-common-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-handler-proxy/4.1.110.Final/netty-handler-proxy-4.1.110.Final.jar\",\n    \"sha512\": \"81bb2568ba68f47c6b73408bbf803b8cb3a8e67d577090b8e302d98e3a87bba7c7b8c67ae41522856d9d55493b53df7d847cc5fe22feec48f44d4fd25fa28ffd\",\n    \"dest\": \"offline-repository/io/netty/netty-handler-proxy/4.1.110.Final\",\n    \"dest-filename\": \"netty-handler-proxy-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-handler-proxy/4.1.110.Final/netty-handler-proxy-4.1.110.Final.pom\",\n    \"sha512\": \"e63c654efdb7d4e08b0abb04fba0e6174790407af39530b98b269e3c71d526d7dfaa00a3e7a85b2216275111d04ab75fd49f8b6ec0f7792050de3d3b9c31c202\",\n    \"dest\": \"offline-repository/io/netty/netty-handler-proxy/4.1.110.Final\",\n    \"dest-filename\": \"netty-handler-proxy-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-handler/4.1.110.Final/netty-handler-4.1.110.Final.jar\",\n    \"sha512\": \"6478df6f5de8337d39de373b578c69b39f31aaa04e3ed018fa26ef3b7fb4cc6a53d7a419e64ad73124b05149b7001dc9025838f305d3158518d406876aaee72b\",\n    \"dest\": \"offline-repository/io/netty/netty-handler/4.1.110.Final\",\n    \"dest-filename\": \"netty-handler-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-handler/4.1.110.Final/netty-handler-4.1.110.Final.pom\",\n    \"sha512\": \"f9381fa1594fb86394a94f932f24d1d99659055a2726dab299a389096fa6c2b3f030f3835b780af20b5144c824603badbb35501df59ac5025562b75830cd414d\",\n    \"dest\": \"offline-repository/io/netty/netty-handler/4.1.110.Final\",\n    \"dest-filename\": \"netty-handler-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-parent/4.1.110.Final/netty-parent-4.1.110.Final.pom\",\n    \"sha512\": \"df94ab612043f8dd6775cfb2a1e4bb50da2dd61aaecfa29fb92469ab4322279c71981ff4924ea48533aa8948a8f186413525313e1216fefad13865059b648c10\",\n    \"dest\": \"offline-repository/io/netty/netty-parent/4.1.110.Final\",\n    \"dest-filename\": \"netty-parent-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-resolver/4.1.110.Final/netty-resolver-4.1.110.Final.jar\",\n    \"sha512\": \"a4d4d072b07895f834d2ac1f8e514f5d7811798303c349d2941b4a938aaebe99281cf6b089efe074ca43aff3f4cced8b11304abe93e94dc061916537d6030635\",\n    \"dest\": \"offline-repository/io/netty/netty-resolver/4.1.110.Final\",\n    \"dest-filename\": \"netty-resolver-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-resolver/4.1.110.Final/netty-resolver-4.1.110.Final.pom\",\n    \"sha512\": \"bd16a422870ac0b051e8b6f35c94ebc7d79100a858b4c3827dea52015b3399e47046484d751e4e324d2d11b531dd9a753e5acfc8e2f9c3ddf79aca667426a8b2\",\n    \"dest\": \"offline-repository/io/netty/netty-resolver/4.1.110.Final\",\n    \"dest-filename\": \"netty-resolver-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-transport-native-unix-common/4.1.110.Final/netty-transport-native-unix-common-4.1.110.Final.jar\",\n    \"sha512\": \"701c3cbc6230eade231538eb7df627d0c411b41b1e15a55c90159e2ab3271a2c7cb09f36be8932d92179940e79f5944282838d475d50ae083a4078247bb9518e\",\n    \"dest\": \"offline-repository/io/netty/netty-transport-native-unix-common/4.1.110.Final\",\n    \"dest-filename\": \"netty-transport-native-unix-common-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-transport-native-unix-common/4.1.110.Final/netty-transport-native-unix-common-4.1.110.Final.pom\",\n    \"sha512\": \"0e3e0664a8e2bb5daf8b7036ce7a1dc0a81bb42bd6aea2fef880eaf3186c05e7553f78d5e886095f7f36af63f5e41abe2a2d829e6b9d1527458ea533fd0d93f7\",\n    \"dest\": \"offline-repository/io/netty/netty-transport-native-unix-common/4.1.110.Final\",\n    \"dest-filename\": \"netty-transport-native-unix-common-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-transport/4.1.110.Final/netty-transport-4.1.110.Final.jar\",\n    \"sha512\": \"942af5e0877ad0c536cb5078ac1618176e2a9cf6f7195128a9540d235decc64c668ac276d98571e63d0ea72489bb136721efc00eda2c74be093f4fc04e76baa3\",\n    \"dest\": \"offline-repository/io/netty/netty-transport/4.1.110.Final\",\n    \"dest-filename\": \"netty-transport-4.1.110.Final.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/netty/netty-transport/4.1.110.Final/netty-transport-4.1.110.Final.pom\",\n    \"sha512\": \"6c9be003ee48761dde21c60063fd1a06c257db1fcabc38b5c63c2f6e3d16d055e70f05e5ae5bf8811307c0cbc6679179a17a2c9b09704c12b6e401ddf243df1a\",\n    \"dest\": \"offline-repository/io/netty/netty-transport/4.1.110.Final\",\n    \"dest-filename\": \"netty-transport-4.1.110.Final.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/perfmark/perfmark-api/0.27.0/perfmark-api-0.27.0.jar\",\n    \"sha512\": \"647b32d75928e44fe355c7f34696d886bd332aa76a6c457e5c90a57d28a8a00e04a5206cf9bba86037660b81eb3f5f82dee7a0dd61330be94f5a71313cf99fd3\",\n    \"dest\": \"offline-repository/io/perfmark/perfmark-api/0.27.0\",\n    \"dest-filename\": \"perfmark-api-0.27.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/perfmark/perfmark-api/0.27.0/perfmark-api-0.27.0.module\",\n    \"sha512\": \"7e4a85333395d27519512bfb2cf70fdeebcc1648bdc6b5b2de6eef5e52e14b2b97f3a69798b479f7721e8b72aec2f20f7d4b55b6eca9e316a547c4b12516e106\",\n    \"dest\": \"offline-repository/io/perfmark/perfmark-api/0.27.0\",\n    \"dest-filename\": \"perfmark-api-0.27.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/io/perfmark/perfmark-api/0.27.0/perfmark-api-0.27.0.pom\",\n    \"sha512\": \"2ec342990091973577ee9bdb2ee61357fcf7051233467ccc6696c9aa0c53b82ee4777c74239eec39b6d5048604b1c5974950b9c6aa1464cc6ecd6457cc764206\",\n    \"dest\": \"offline-repository/io/perfmark/perfmark-api/0.27.0\",\n    \"dest-filename\": \"perfmark-api-0.27.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/jakarta/activation/jakarta.activation-api/1.2.1/jakarta.activation-api-1.2.1.jar\",\n    \"sha512\": \"c60edc99f119b9e0df0cf527e2512f2b7ab9db0e17c54e83850695f80f652c981eaae90a296db671cf7ed88a044c150224e030df333feb30f346e8a31fb794c6\",\n    \"dest\": \"offline-repository/jakarta/activation/jakarta.activation-api/1.2.1\",\n    \"dest-filename\": \"jakarta.activation-api-1.2.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/jakarta/activation/jakarta.activation-api/1.2.1/jakarta.activation-api-1.2.1.pom\",\n    \"sha512\": \"a8af00c78391a59929b6aed1743e50650cbab1f2c9f801757fe6fa3780cc0926489092cac834121260c1c3c702f4ffb4ecacd786ad79d7d38e828e6c39bd399e\",\n    \"dest\": \"offline-repository/jakarta/activation/jakarta.activation-api/1.2.1\",\n    \"dest-filename\": \"jakarta.activation-api-1.2.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api-parent/2.3.2/jakarta.xml.bind-api-parent-2.3.2.pom\",\n    \"sha512\": \"e0a4baa45935dde64bfd9305cf01ae12bb018844546824df5e4f735924eda55c55e2343616ac92de80deb1c40bf2debe8ee5257a96d11ac4879156ba8d1b6f2c\",\n    \"dest\": \"offline-repository/jakarta/xml/bind/jakarta.xml.bind-api-parent/2.3.2\",\n    \"dest-filename\": \"jakarta.xml.bind-api-parent-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api/2.3.2/jakarta.xml.bind-api-2.3.2.jar\",\n    \"sha512\": \"5a9a94fc323aecc9c5b28e9ac688aac8d09725d4cae660a57f5698914db91e351283dfe4909a2cc6de803890ac2b3b9f06af9d071d465031e55326a1085a11db\",\n    \"dest\": \"offline-repository/jakarta/xml/bind/jakarta.xml.bind-api/2.3.2\",\n    \"dest-filename\": \"jakarta.xml.bind-api-2.3.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/jakarta/xml/bind/jakarta.xml.bind-api/2.3.2/jakarta.xml.bind-api-2.3.2.pom\",\n    \"sha512\": \"7b144c06ac7e7ddf63596406d3b441e3e5a3ba7b408a659bc0ceb5b528936b944108398bc06298054de09228293ab3feed01e4e16be493ecc5fb9780d8c52379\",\n    \"dest\": \"offline-repository/jakarta/xml/bind/jakarta.xml.bind-api/2.3.2\",\n    \"dest-filename\": \"jakarta.xml.bind-api-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar\",\n    \"sha512\": \"679cf44c3b9d635b43ed122a555d570292c3f0937c33871c40438a1a53e2058c80578694ec9466eac9e280e19bfb7a95b261594cc4c1161c85dc97df6235e553\",\n    \"dest\": \"offline-repository/javax/annotation/javax.annotation-api/1.3.2\",\n    \"dest-filename\": \"javax.annotation-api-1.3.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.pom\",\n    \"sha512\": \"b97c6fb3b5c6f9ba5c6ec2aa35713816fca94927c1393d2eaa04cac6a6c44c464baeb34d62d9af33268a7eaa817318df1b2116641ff7e8ade84c70b358e60bac\",\n    \"dest\": \"offline-repository/javax/annotation/javax.annotation-api/1.3.2\",\n    \"dest-filename\": \"javax.annotation-api-1.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/javax/inject/javax.inject/1/javax.inject-1.jar\",\n    \"sha512\": \"e126b7ccf3e42fd1984a0beef1004a7269a337c202e59e04e8e2af714280d2f2d8d2ba5e6f59481b8dcd34aaf35c966a688d0b48ec7e96f102c274dc0d3b381e\",\n    \"dest\": \"offline-repository/javax/inject/javax.inject/1\",\n    \"dest-filename\": \"javax.inject-1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/javax/inject/javax.inject/1/javax.inject-1.pom\",\n    \"sha512\": \"02f0c773ba24b74f45f6519c653cb118395f81389c7e73a034f82074a3e277f793d77783d794143236b05fc5247af5f69d9b2605d0929b742a5673a55e51f880\",\n    \"dest\": \"offline-repository/javax/inject/javax.inject/1\",\n    \"dest-filename\": \"javax.inject-1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/java/dev/jna/jna-platform/5.6.0/jna-platform-5.6.0.jar\",\n    \"sha512\": \"e55cb2eb60742d4a1de61e5d85b4c71d1a6769ef793b9cb5454ed1775ed593201e3c833e42ea033c99ee256217fde17bac5b93c8f12cbb90795c14956eb31d61\",\n    \"dest\": \"offline-repository/net/java/dev/jna/jna-platform/5.6.0\",\n    \"dest-filename\": \"jna-platform-5.6.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/java/dev/jna/jna-platform/5.6.0/jna-platform-5.6.0.pom\",\n    \"sha512\": \"1d1c2f90cb34ef8ac5a6f7cd6e4381aae763a16b81f1e685c0bfe69adf2e7a4b5b80234f731bccf28ff9361a2b337447749230726e088e6784da45b93a9591de\",\n    \"dest\": \"offline-repository/net/java/dev/jna/jna-platform/5.6.0\",\n    \"dest-filename\": \"jna-platform-5.6.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/java/dev/jna/jna/5.6.0/jna-5.6.0.jar\",\n    \"sha512\": \"f250d92a70ef686466d44592a10513420dc6d6ec188e479f4ceb5ee6615505f3aad2941949364c89f09781b3f8bb09e0679f779ce81c1231f714f9a4f7d769ba\",\n    \"dest\": \"offline-repository/net/java/dev/jna/jna/5.6.0\",\n    \"dest-filename\": \"jna-5.6.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/java/dev/jna/jna/5.6.0/jna-5.6.0.pom\",\n    \"sha512\": \"b9cf65b293f20d563877d5e1a23292a794e6470c77496f6fe82978bb0f8e13d9402bfe2fedc716ddbce6989383ff90bf65d3ecd2c821b0c95e00f8951e90b306\",\n    \"dest\": \"offline-repository/net/java/dev/jna/jna/5.6.0\",\n    \"dest-filename\": \"jna-5.6.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/java/jvnet-parent/1/jvnet-parent-1.pom\",\n    \"sha512\": \"22fb9b68f57380088955b5526bfc382da87332202bb4741def44af2ec340240d8d2ad1283ffcab0433175fe35ab5540a503aaf9a2e44aad1de005c8915bfabe8\",\n    \"dest\": \"offline-repository/net/java/jvnet-parent/1\",\n    \"dest-filename\": \"jvnet-parent-1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/java/jvnet-parent/3/jvnet-parent-3.pom\",\n    \"sha512\": \"93b78fac40ca4de12d5a2fb4e339ba9e3c40a25ddcfe58272dc2a8e4b36d2c7cc51075aa2a25f0b3c1d4bd3142551e77847d1bd5599c60f5d50d548b72b74bfa\",\n    \"dest\": \"offline-repository/net/java/jvnet-parent/3\",\n    \"dest-filename\": \"jvnet-parent-3.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/sf/jopt-simple/jopt-simple/4.9/jopt-simple-4.9.jar\",\n    \"sha512\": \"fd04c19bce810a1548b2d2eaadb915cff2cbc81a81ec5258aafc1ba329100daedc49edad1fc7b254ab892996796124283d7004b5414f662c0efa3979add9ca5f\",\n    \"dest\": \"offline-repository/net/sf/jopt-simple/jopt-simple/4.9\",\n    \"dest-filename\": \"jopt-simple-4.9.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/sf/jopt-simple/jopt-simple/4.9/jopt-simple-4.9.pom\",\n    \"sha512\": \"fc6dbc416babd1d49152b832cfbb425686a718cce2157a96743be2d541e65dc072ed617b6eca8003c3bbb4be65394c5468ca2ec52eceaba4d3baffb10ff8c489\",\n    \"dest\": \"offline-repository/net/sf/jopt-simple/jopt-simple/4.9\",\n    \"dest-filename\": \"jopt-simple-4.9.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/sf/kxml/kxml2/2.3.0/kxml2-2.3.0.jar\",\n    \"sha512\": \"f97d418d4c2892fa184f5be83166ac2cd771fd10d7625104d9b054ec0ff361927a2ac2539d38f326f61373b6d700a3b5075605763562ac0ae6714903773cd1cb\",\n    \"dest\": \"offline-repository/net/sf/kxml/kxml2/2.3.0\",\n    \"dest-filename\": \"kxml2-2.3.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/net/sf/kxml/kxml2/2.3.0/kxml2-2.3.0.pom\",\n    \"sha512\": \"ed580cade03dd0358b3221ae507804b9fcc15d47470a6b726dbe2920756ab3ae3b7f7b57ba04ad7a631d46c4d70cc88d5e460d0fa34c05e1aa47d222dc7ee872\",\n    \"dest\": \"offline-repository/net/sf/kxml/kxml2/2.3.0\",\n    \"dest-filename\": \"kxml2-2.3.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/apache/13/apache-13.pom\",\n    \"sha512\": \"3b25f9f51a7ee9647fe2e1287e75a67ccdf3f08055bec20c6a60b290876afc691f16b23ab3df7b733695b828411b716a0b3509c22ec6fb0c5dce4f21811ae434\",\n    \"dest\": \"offline-repository/org/apache/apache/13\",\n    \"dest-filename\": \"apache-13.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/apache/18/apache-18.pom\",\n    \"sha512\": \"9ef6f99b30fe2603ad8f2c88116072de36bd2dc99590fd9e7eecf153dbf50cbd766694d861e666138d2a26137be69fe98cc38a491f6a2a68e8d421d656731ed1\",\n    \"dest\": \"offline-repository/org/apache/apache/18\",\n    \"dest-filename\": \"apache-18.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/apache/21/apache-21.pom\",\n    \"sha512\": \"c82bd27c06d76b3f467118b1a8e0976c60fd0e7d7a01dac685c9a91e1822c0e6f5829bf48ad532a9fc000089cdb894a97c5686365fd3c8c8bfa787136a4fa9d2\",\n    \"dest\": \"offline-repository/org/apache/apache/21\",\n    \"dest-filename\": \"apache-21.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/apache/23/apache-23.pom\",\n    \"sha512\": \"d17d23bcd3d1cd95d15783aaa6f861e3d0bea60f8a1adf6182b298cdf7ad0a1eb74a775b920bfe8324c414747979623a6a4615d18c492dc25d71e48dd6a504b8\",\n    \"dest\": \"offline-repository/org/apache/apache/23\",\n    \"dest-filename\": \"apache-23.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/apache/31/apache-31.pom\",\n    \"sha512\": \"12563230cf16a646d4a20453e6ae98e9f8eb5abf89502620314c7040f2c006cd0795c7615e02a0f4c6e1d87328b156fa89b83a9990cfcfb2a0e8fc7b7b9f97f9\",\n    \"dest\": \"offline-repository/org/apache/apache/31\",\n    \"dest-filename\": \"apache-31.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.21/commons-compress-1.21.jar\",\n    \"sha512\": \"c92d9a12547aab475e057955ad815fdfe92ff44c78383fa5af54b089f1bff5525126ef6aef93334f3bfc22e2fef4ad0d969f69384e978a83a55f011a53e7e471\",\n    \"dest\": \"offline-repository/org/apache/commons/commons-compress/1.21\",\n    \"dest-filename\": \"commons-compress-1.21.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.21/commons-compress-1.21.pom\",\n    \"sha512\": \"530a1505ca1e1c4eb9336b7a7cae3116ea9fc81d77d0e2530f1c050a8b5593cd65adc90947f13fb7e10e40db479c54415cc9ad0f58bf5d1f924f3986ed634bfd\",\n    \"dest\": \"offline-repository/org/apache/commons/commons-compress/1.21\",\n    \"dest-filename\": \"commons-compress-1.21.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/34/commons-parent-34.pom\",\n    \"sha512\": \"364ede203a23157ec601d28ff141c0c69759fc5c483e44e346fa1592403f343f0722f7763243b2ee7a190c7a744b1cce1f40247f5a6c7b3dbfbf487c505a40bf\",\n    \"dest\": \"offline-repository/org/apache/commons/commons-parent/34\",\n    \"dest-filename\": \"commons-parent-34.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/42/commons-parent-42.pom\",\n    \"sha512\": \"a35d1f7919551adbeada88d9f47a4d6de200380aa43266f91d4b65255120cbf4b11f73bb3f9b98eeb960fd9f1b0793dd48095b46270ed2376ce3b122861a94d6\",\n    \"dest\": \"offline-repository/org/apache/commons/commons-parent/42\",\n    \"dest-filename\": \"commons-parent-42.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/52/commons-parent-52.pom\",\n    \"sha512\": \"73304055a0bd1da4d0e6f6f9adfe4327f970f40b9703b9df15dd2048b8a617c32a3ee98a95d0f70d3bae719940ed5d072487e4a222d9a9a2c7e5fa15eca0b658\",\n    \"dest\": \"offline-repository/org/apache/commons/commons-parent/52\",\n    \"dest-filename\": \"commons-parent-52.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/commons/commons-parent/69/commons-parent-69.pom\",\n    \"sha512\": \"c7f3f2d929f60251a2afe80d3b53fac48752ca61e668ae56588d23b162cd3c911d6c3da76148a340d19512555152eccfd435b4d9fbe628ce10054bac83ed07e7\",\n    \"dest\": \"offline-repository/org/apache/commons/commons-parent/69\",\n    \"dest-filename\": \"commons-parent-69.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar\",\n    \"sha512\": \"a084ef30fb0a2a25397d8fab439fe68f67e294bf53153e2e1355b8df92886d40fe6abe35dc84f014245f7158e92641bcbd98019b4fbbd9e5a0db495b160b4ced\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpclient/4.5.14\",\n    \"dest-filename\": \"httpclient-4.5.14.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.pom\",\n    \"sha512\": \"46500859b1206c1ec6a69c66d6b6d224ac835c9316a88403a2060c658b4c4e2a7f69ce0b3b5f1900abf479ebda10757c25ab595de9527b53f9e80abdf48383e8\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpclient/4.5.14\",\n    \"dest-filename\": \"httpclient-4.5.14.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.14/httpcomponents-client-4.5.14.pom\",\n    \"sha512\": \"7ae6fe0a7865aa7aeb1e4f3f3694856e0b0c5f7d5e452682e3734b45e433d2001352bea46fc32c1694bd10577b16030f77161a0fdca7c369afc2cc5edf4c55e2\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcomponents-client/4.5.14\",\n    \"dest-filename\": \"httpcomponents-client-4.5.14.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-client/4.5.6/httpcomponents-client-4.5.6.pom\",\n    \"sha512\": \"5c4762d49bacb5f2138db3d701bc1d1108d08fea2909d274528600ecbd21de05d39b74009023af7b8552dd43c480dc3eb04229ecc1a4e6535daa4a90526aff49\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcomponents-client/4.5.6\",\n    \"dest-filename\": \"httpcomponents-client-4.5.6.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-core/4.4.16/httpcomponents-core-4.4.16.pom\",\n    \"sha512\": \"7bc3e413442010aeaaa9fa6fd11e4c32aea9cd50ad19f485869d469a13886ca70f645169c1130ccac864d73570b8dde21562313ef3f8c031ffaf8500b60e14d3\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcomponents-core/4.4.16\",\n    \"dest-filename\": \"httpcomponents-core-4.4.16.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-parent/10/httpcomponents-parent-10.pom\",\n    \"sha512\": \"2a13d94b4af958e39d49408842080f097c662cf2276befb89b13b94b95eaba64eeb389b5bc6638170658a00de42104001daa3eb0650fae32ba4cb44503022c71\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcomponents-parent/10\",\n    \"dest-filename\": \"httpcomponents-parent-10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcomponents-parent/11/httpcomponents-parent-11.pom\",\n    \"sha512\": \"bc676698caec72d525b9bf408432e9cd9c7b5d2227e0778fd79c303041cb5b07b88f98433d59c0149d6c11c27c3834722ceb283048919e467785efd7f4c399a2\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcomponents-parent/11\",\n    \"dest-filename\": \"httpcomponents-parent-11.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar\",\n    \"sha512\": \"168026436a6bcf5e96c0c59606638abbdc30de4b405ae55afde70fdf2895e267a3d48bba6bdadc5a89f38e31da3d9a9dc91e1cab7ea76f5e04322cf1ec63b838\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcore/4.4.16\",\n    \"dest-filename\": \"httpcore-4.4.16.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.pom\",\n    \"sha512\": \"8d8809246d6a665e0870dc396da4292c13bee7db7d82fdfffd90049b33141d45d5c1b022d095d0f270fa758472dcd3c6ec742efa045e03eedbe66ceeeca426d4\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpcore/4.4.16\",\n    \"dest-filename\": \"httpcore-4.4.16.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpmime/4.5.6/httpmime-4.5.6.jar\",\n    \"sha512\": \"9841db7779b647de4668ded9b79e8c510a653076384fc3059ef186ea5d82828e149de48c016b5cb89a1beabe60981429265a8324be3108473c57af77a62abd6a\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpmime/4.5.6\",\n    \"dest-filename\": \"httpmime-4.5.6.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpmime/4.5.6/httpmime-4.5.6.pom\",\n    \"sha512\": \"4d020fc801b1ca440df4ebe044b0dffae5cea59e9c48205beeceb1931ea3682f11eb7fb36f054693cf9f32886da6b48b727265b60a0ffa49300b220325a09266\",\n    \"dest\": \"offline-repository/org/apache/httpcomponents/httpmime/4.5.6\",\n    \"dest-filename\": \"httpmime-4.5.6.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bitbucket/b_c/jose4j/0.9.5/jose4j-0.9.5.jar\",\n    \"sha512\": \"d475aa9e74a173e27aa72f4361eef288d259f337f8082eafa572aad763150ea679829e2433cb03cb0249eb58a83d68bfbc54d124154dd71b745fc7182620c59f\",\n    \"dest\": \"offline-repository/org/bitbucket/b_c/jose4j/0.9.5\",\n    \"dest-filename\": \"jose4j-0.9.5.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bitbucket/b_c/jose4j/0.9.5/jose4j-0.9.5.pom\",\n    \"sha512\": \"b3840f21c533a48a393dc1cf50cdf73cca9779af150cb8d0f51cc5932b59b2ade1a97dffb005d075b6fa3b9d42e26f81a8014fd98243734995c30462930871d6\",\n    \"dest\": \"offline-repository/org/bitbucket/b_c/jose4j/0.9.5\",\n    \"dest-filename\": \"jose4j-0.9.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.79/bcpkix-jdk18on-1.79.jar\",\n    \"sha512\": \"12b6b18d6bb89d4c82d616210467fb7c3951d1b6a9dff10b4b7633ec708aabea07a0f39c48344ab18fdfec2975f6ef8911ba2ca9189ff75c522574b6d76f4abc\",\n    \"dest\": \"offline-repository/org/bouncycastle/bcpkix-jdk18on/1.79\",\n    \"dest-filename\": \"bcpkix-jdk18on-1.79.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.79/bcpkix-jdk18on-1.79.pom\",\n    \"sha512\": \"0d3bcb7e7a468cd7b9fddd580a0e30b9cd350eb867396e62004798e983dfdfd42e14db5aa3decf958414b8bfbe5569400784ab2602332ebb20057c7972376f30\",\n    \"dest\": \"offline-repository/org/bouncycastle/bcpkix-jdk18on/1.79\",\n    \"dest-filename\": \"bcpkix-jdk18on-1.79.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.79/bcprov-jdk18on-1.79.jar\",\n    \"sha512\": \"27bc54158da8165a55a7edae9fd09980795979cee099e98d81fadb4bd4dce4d5c86b8a635d1e892e286676d23b6ae5961a762fadf3a864513c8e0c6dbbdefd3a\",\n    \"dest\": \"offline-repository/org/bouncycastle/bcprov-jdk18on/1.79\",\n    \"dest-filename\": \"bcprov-jdk18on-1.79.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.79/bcprov-jdk18on-1.79.pom\",\n    \"sha512\": \"def89485d4db1be6299d539e261c52bc89830e21256005f26315f81637e0292d37e4ce754fd00dbc8a51e6c65cdd94e5fb5837fbc4d63d4835e90e8e96a7e040\",\n    \"dest\": \"offline-repository/org/bouncycastle/bcprov-jdk18on/1.79\",\n    \"dest-filename\": \"bcprov-jdk18on-1.79.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bouncycastle/bcutil-jdk18on/1.79/bcutil-jdk18on-1.79.jar\",\n    \"sha512\": \"a3ec7c22f6e716e2c06b9e93b1992bda23eb92ea0cc3f3afc5bd7ae44a9235ff2216d0c0097799a30fd2e2dd618e7f30cc210007da61e1dee2e72b8fbb0de16f\",\n    \"dest\": \"offline-repository/org/bouncycastle/bcutil-jdk18on/1.79\",\n    \"dest-filename\": \"bcutil-jdk18on-1.79.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/bouncycastle/bcutil-jdk18on/1.79/bcutil-jdk18on-1.79.pom\",\n    \"sha512\": \"2deabbe38b38c0733bdecd5a1eee264f251ecedefa551479b2ea2415f5f659b856eb329e2efb265b26313fd8c1082007b1145bfd3df181875b8f4dc861b8f02d\",\n    \"dest\": \"offline-repository/org/bouncycastle/bcutil-jdk18on/1.79\",\n    \"dest-filename\": \"bcutil-jdk18on-1.79.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/3.43.0/checker-qual-3.43.0.jar\",\n    \"sha512\": \"823ea28e3c822ff48e4e985a421fa0b53ca3419e2c0635c3d4d0a9822399b6491780e26a9161d4733857c97987bff2d725ff4453b2c22ed412eef46ea27f5d84\",\n    \"dest\": \"offline-repository/org/checkerframework/checker-qual/3.43.0\",\n    \"dest-filename\": \"checker-qual-3.43.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/3.43.0/checker-qual-3.43.0.module\",\n    \"sha512\": \"b5f80edd3661a81288365222b95a85e19096e4bd5485f59b7d72b807553ba8c0dbb1cee300a987f6a0ced3d4114b494cbabe82f52e48f596ae17fab1f1898eba\",\n    \"dest\": \"offline-repository/org/checkerframework/checker-qual/3.43.0\",\n    \"dest-filename\": \"checker-qual-3.43.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/checkerframework/checker-qual/3.43.0/checker-qual-3.43.0.pom\",\n    \"sha512\": \"b7bdf783e5ea98ee0e11d264f4a0022fd5ae4c228a00e2e8ba2f36c1a3991903ac334ea030bcbf1837e4097599f7cd0a599e0b4c03b35d5dc7550b4d93efda0f\",\n    \"dest\": \"offline-repository/org/checkerframework/checker-qual/3.43.0\",\n    \"dest-filename\": \"checker-qual-3.43.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.24/animal-sniffer-annotations-1.24.jar\",\n    \"sha512\": \"6f8118093576be9dc0860cd02600d9e159d78e99b94f06c2dcbbfea6cc3a7d509418dd697b998a9abcdf0315497e4ab21e16f3fd2942e97bcf7fc16b1122bcca\",\n    \"dest\": \"offline-repository/org/codehaus/mojo/animal-sniffer-annotations/1.24\",\n    \"dest-filename\": \"animal-sniffer-annotations-1.24.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-annotations/1.24/animal-sniffer-annotations-1.24.pom\",\n    \"sha512\": \"dc61cc5f6e937cff5b14c4a5b6cca6ddb4ab4a8a9c1b768ea2b86bf2b225c5d788f14a085cd2681c9a73d1fd7be7f45ce906b1009d31ff6705ae5671c32ce9e2\",\n    \"dest\": \"offline-repository/org/codehaus/mojo/animal-sniffer-annotations/1.24\",\n    \"dest-filename\": \"animal-sniffer-annotations-1.24.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/codehaus/mojo/animal-sniffer-parent/1.24/animal-sniffer-parent-1.24.pom\",\n    \"sha512\": \"b3869abce76c232d11ff8f5e8b6dd86bcc6e809dea7c0ba3c39e04e508d0066ce4eb3c60004287a2d8b1bd3e72733281f5c59f9e002c46de4ffd251e0eafaeff\",\n    \"dest\": \"offline-repository/org/codehaus/mojo/animal-sniffer-parent/1.24\",\n    \"dest-filename\": \"animal-sniffer-parent-1.24.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/codehaus/mojo/mojo-parent/84/mojo-parent-84.pom\",\n    \"sha512\": \"2cb800cf9930fa4229facd2b0eb1d99bc04f02b302acdaa240061cb7266d5b0dfe84782e5889e8c7d15ace2328f0f6c2278e9ff9751ef41ded2b5c660e52d0c9\",\n    \"dest\": \"offline-repository/org/codehaus/mojo/mojo-parent/84\",\n    \"dest-filename\": \"mojo-parent-84.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.2/project-1.0.2.pom\",\n    \"sha512\": \"0725e91db9dee43a75ba70ec073da98dbe158b157f6038b9a5fc906e4307add423b213829bea25944f42c88a37417788398c5136267ce8005a530505c5b5fa28\",\n    \"dest\": \"offline-repository/org/eclipse/ee4j/project/1.0.2\",\n    \"dest-filename\": \"project-1.0.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.5/project-1.0.5.pom\",\n    \"sha512\": \"5fd90f300231200c1158602372fa9b6ae2cf2746200c7c98e7dcb8639ece995d44c54817cc44c5ad40c6b18e311bee771933550acbb55954fa25eaa9b3140dea\",\n    \"dest\": \"offline-repository/org/eclipse/ee4j/project/1.0.5\",\n    \"dest-filename\": \"project-1.0.5.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/2.3.2/jaxb-bom-2.3.2.pom\",\n    \"sha512\": \"91a18e46630179e6637e5c07bb156eec873dfcdf5676a0c51ec14bb8687e961a7e01c56a091c6502c85e1bf1e257bbf6c130e93e74bb364a3f41efba7c7a40da\",\n    \"dest\": \"offline-repository/org/glassfish/jaxb/jaxb-bom/2.3.2\",\n    \"dest-filename\": \"jaxb-bom-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.3.2/jaxb-runtime-2.3.2.jar\",\n    \"sha512\": \"8a6cf3dd9fac4fb6ddb5f0861a2cac093e04d186fc787ee78c30862d225eb677b9605e1177cf57da0d4f45c613f107187c330be0684b43b0cab9a922cd96db66\",\n    \"dest\": \"offline-repository/org/glassfish/jaxb/jaxb-runtime/2.3.2\",\n    \"dest-filename\": \"jaxb-runtime-2.3.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.3.2/jaxb-runtime-2.3.2.pom\",\n    \"sha512\": \"4827043a65d75575a8110a8bd7baf78a7a36b05f21facdb948c5a9474793cd3e1d67c98c9b571544e0c16701872222671906880e7b70639f3bb41bbaa387b7f9\",\n    \"dest\": \"offline-repository/org/glassfish/jaxb/jaxb-runtime/2.3.2\",\n    \"dest-filename\": \"jaxb-runtime-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/glassfish/jaxb/txw2/2.3.2/txw2-2.3.2.jar\",\n    \"sha512\": \"66c174093c47b75b900159c5449bd99eb15308d15b395771367adc5862612edb985c3678428c4f60e7c27b46d123afac39277f80db766511a6b6f0756d525559\",\n    \"dest\": \"offline-repository/org/glassfish/jaxb/txw2/2.3.2\",\n    \"dest-filename\": \"txw2-2.3.2.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/glassfish/jaxb/txw2/2.3.2/txw2-2.3.2.pom\",\n    \"sha512\": \"8fbd16b2d9321d5fa9e9cb5e15e0aeaf1ad15f8147226d51ef9cd786f456bf0ae7fdb66742c8bb58226eb54dcc634443d6635924cb9f0b2b5082faaa807ff356\",\n    \"dest\": \"offline-repository/org/glassfish/jaxb/txw2/2.3.2\",\n    \"dest-filename\": \"txw2-2.3.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jdom/jdom2/2.0.6/jdom2-2.0.6.jar\",\n    \"sha512\": \"315791dc16bc6240d81da7fee9ae325102ff7db19a57805335d189bc747abc4d1c80144589ebf956613b93b2263c7565fdf171aca0c6c598616eb3f0bdf4cc58\",\n    \"dest\": \"offline-repository/org/jdom/jdom2/2.0.6\",\n    \"dest-filename\": \"jdom2-2.0.6.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jdom/jdom2/2.0.6/jdom2-2.0.6.pom\",\n    \"sha512\": \"1c111e5c52440b00b5573e05a9f08c048d5630b1d77d4dbcc8b9efe8b68584d7926d8b4c867ef6b212ecc5cf549360eb8ecd654f96f04c2a354aa52e493bce93\",\n    \"dest\": \"offline-repository/org/jdom/jdom2/2.0.6\",\n    \"dest-filename\": \"jdom2-2.0.6.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar\",\n    \"sha512\": \"5622d0ffe410e7272e2bb9fae1006caedeb86d0c62d2d9f3929a3b3cdcdef1963218fcf0cede82e95ef9f4da3ed4a173fa055ee6e4038886376181e0423e02ff\",\n    \"dest\": \"offline-repository/org/jetbrains/annotations/13.0\",\n    \"dest-filename\": \"annotations-13.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.pom\",\n    \"sha512\": \"63ef480f698215d4cd4501b06e86df1a741ac2b86216fd3ff6eee146da746caa390df27351e25598971edb368aeae41055ff1ed77e4bf5d7edb6abc832d150ce\",\n    \"dest\": \"offline-repository/org/jetbrains/annotations/13.0\",\n    \"dest-filename\": \"annotations-13.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/compose-gradle-plugin/1.10.1/compose-gradle-plugin-1.10.1.jar\",\n    \"sha512\": \"00921bc9e3a7cffc435b54746a32e34f607ab7152963b19d4bf4b0c47f9eb81a0353efcbf11fdcb3660846dd0cd4414bce800a3dfc37110215f466284c6a32a4\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/compose-gradle-plugin/1.10.1\",\n    \"dest-filename\": \"compose-gradle-plugin-1.10.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/compose-gradle-plugin/1.10.1/compose-gradle-plugin-1.10.1.module\",\n    \"sha512\": \"a2f4ca3d03ddb5b0622da73a4f118307ad9941ef05a6922f1bdb5c6fb56348a2f1574b8f826a555d05fc1a7a7a3905f5e350af54ef3b560bff77ba78f9b43eff\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/compose-gradle-plugin/1.10.1\",\n    \"dest-filename\": \"compose-gradle-plugin-1.10.1.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/compose-gradle-plugin/1.10.1/compose-gradle-plugin-1.10.1.pom\",\n    \"sha512\": \"fc006a917da0a9f992c784e022b18158318a072d1f8120975f70b4de171b9046f830f82a6019183c4fc92384d42ccd8f6c322e491f79d6c47622302530b7bd4d\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/compose-gradle-plugin/1.10.1\",\n    \"dest-filename\": \"compose-gradle-plugin-1.10.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/hot-reload/hot-reload-gradle-plugin/1.0.0/hot-reload-gradle-plugin-1.0.0.jar\",\n    \"sha512\": \"7570a9ed3190047e0e4f85069edb223cfaf1402e5ec97afe0c7eb29daff5336518771bfd22b5d345bc1c3133cb2b377ea0965073542ca724582b4bc56e478d52\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/hot-reload/hot-reload-gradle-plugin/1.0.0\",\n    \"dest-filename\": \"hot-reload-gradle-plugin-1.0.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/hot-reload/hot-reload-gradle-plugin/1.0.0/hot-reload-gradle-plugin-1.0.0.module\",\n    \"sha512\": \"79fe305d0973905db843a6536c9cb196de81521b82d704c614bef317c55b262a1e426eb26a09fec399c440699222c0cdd743a314a8ffacd7a9beb7503b1acccf\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/hot-reload/hot-reload-gradle-plugin/1.0.0\",\n    \"dest-filename\": \"hot-reload-gradle-plugin-1.0.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/hot-reload/hot-reload-gradle-plugin/1.0.0/hot-reload-gradle-plugin-1.0.0.pom\",\n    \"sha512\": \"fcb859685dc40ab549173dbcba69ef6f967f255b3151ee1b1fc7e688d4b4d736885286fb1beaee7d95f0a717de29580e452a8f4f33cb3e826e9c334cfa848155\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/hot-reload/hot-reload-gradle-plugin/1.0.0\",\n    \"dest-filename\": \"hot-reload-gradle-plugin-1.0.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/hot-reload/org.jetbrains.compose.hot-reload.gradle.plugin/1.0.0/org.jetbrains.compose.hot-reload.gradle.plugin-1.0.0.pom\",\n    \"sha512\": \"8e9dd6e31425c5e2036365e9e2a396117143b63f4daf01573e76c3deea564825e7165facc405360d4b09b25a1ae3fec961f359e4d20fd5c826f399277f9b519b\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/hot-reload/org.jetbrains.compose.hot-reload.gradle.plugin/1.0.0\",\n    \"dest-filename\": \"org.jetbrains.compose.hot-reload.gradle.plugin-1.0.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/compose/org.jetbrains.compose.gradle.plugin/1.10.1/org.jetbrains.compose.gradle.plugin-1.10.1.pom\",\n    \"sha512\": \"fd3021f97362ec034f0c0a3de69e358c7a66e4b9f967acc57e030440bce26a21dd5ccfcaea7f2911440477da173f0db6ca9dbecfe31efd6c622d0ea10b6b260d\",\n    \"dest\": \"offline-repository/org/jetbrains/compose/org.jetbrains.compose.gradle.plugin/1.10.1\",\n    \"dest-filename\": \"org.jetbrains.compose.gradle.plugin-1.10.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/abi-tools-api/2.3.10/abi-tools-api-2.3.10.jar\",\n    \"sha512\": \"201ca364021448c2918df71d30089df589a4e3ebe56c3ad8d09014ed672ab7a16dfe1c5ed3362d8ef885dee1a0cf06e188621d1ae9c932ced974a129f30aa18d\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/abi-tools-api/2.3.10\",\n    \"dest-filename\": \"abi-tools-api-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/abi-tools-api/2.3.10/abi-tools-api-2.3.10.pom\",\n    \"sha512\": \"d5f411fa775ec484f75484cf7baae80f0de0e4d3a03328b468474c908305ba25dd035aadbe24dd7bff024d7283fb041b15689d8ba69bd30dedd70829c500570d\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/abi-tools-api/2.3.10\",\n    \"dest-filename\": \"abi-tools-api-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/atomicfu/2.3.10/atomicfu-2.3.10.pom\",\n    \"sha512\": \"3b8a532071834d576e704368136a40297b3dbcdfc7db025652ed378a2552af5e37539e508f8a42e00d7c2be6f450b2e45d75400a9111680610840c08c66ff45a\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/atomicfu/2.3.10\",\n    \"dest-filename\": \"atomicfu-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/compose-compiler-gradle-plugin/2.3.10/compose-compiler-gradle-plugin-2.3.10-gradle813.jar\",\n    \"sha512\": \"37bc5b39022addd7e832504946b93f7c524908a8b32aa62db7684c97c2bb060039cc7547e762bfd925b50891c339d63fcca0b68cb2a5c9aa8532378f10b0efe5\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/compose-compiler-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"compose-compiler-gradle-plugin-2.3.10-gradle813.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/compose-compiler-gradle-plugin/2.3.10/compose-compiler-gradle-plugin-2.3.10.module\",\n    \"sha512\": \"7f35ced92fd9b1640dedcb617b1c65c0d107867879cb349c738684f7665a33a9a707c9c68d24919315c30a68ebf183e84f9acc1c2035c86d8b50e7b882691dba\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/compose-compiler-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"compose-compiler-gradle-plugin-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/compose-compiler-gradle-plugin/2.3.10/compose-compiler-gradle-plugin-2.3.10.pom\",\n    \"sha512\": \"46ebaf0d0277e1f8f041c2449ff89899376eb660095d7c86539fa20defe512ff6b5dba9ad1cf1b71b6e4a877e8b2900af26c5d95bd08002861fff181c1da23ee\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/compose-compiler-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"compose-compiler-gradle-plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/fus-statistics-gradle-plugin/2.3.10/fus-statistics-gradle-plugin-2.3.10-gradle813.jar\",\n    \"sha512\": \"60aa5060e364d1f936c925d6ec30d0f685359f84fe37001b6c42f2a47de6804627526774ebb9f3124cdda67ff1867fa2acf4d5da6fd44cc69bb26c155f5069b2\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/fus-statistics-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"fus-statistics-gradle-plugin-2.3.10-gradle813.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/fus-statistics-gradle-plugin/2.3.10/fus-statistics-gradle-plugin-2.3.10.module\",\n    \"sha512\": \"5fb4d41cceafebb14c80adc458f8b08c37fb9d7ceab2e19620342e262d38a123f0b861ca8015f53f2a606fb83ce8f1c41bc8437e0b1956ade2de2f2491215b12\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/fus-statistics-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"fus-statistics-gradle-plugin-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/fus-statistics-gradle-plugin/2.3.10/fus-statistics-gradle-plugin-2.3.10.pom\",\n    \"sha512\": \"8eeb13dfb218d9d739186a5ba8152bd2b044a6b23e297d040b73db602b0fc03ec725222464c270b7707945d4e662253859a501a06afa8649ade77a9d877aa9b7\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/fus-statistics-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"fus-statistics-gradle-plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-allopen/2.3.10/kotlin-allopen-2.3.10.pom\",\n    \"sha512\": \"b10558b0fe55dcd52cd79a3c15cb2c1e36c88e54a5465f14b404f0ffec1c2a1df5cca33fcf642ffafb0483fc037b9e2153f5389cc5f01880fb7f7a8f20819751\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-allopen/2.3.10\",\n    \"dest-filename\": \"kotlin-allopen-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-assignment/2.3.10/kotlin-assignment-2.3.10.pom\",\n    \"sha512\": \"89de8a58f65a73eab2656395d1a0ce17681402589a0fd9f20eb867de4cda062877dd20e644e37bcddae7c992552fb0242a32431155b223caaa2a817f28c2e4f9\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-assignment/2.3.10\",\n    \"dest-filename\": \"kotlin-assignment-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-statistics/2.3.10/kotlin-build-statistics-2.3.10.jar\",\n    \"sha512\": \"81d0d361fc05e62acc2411abf34adbfa91c7ba9fed065b9f3917f3b8dfabf298d14c52fb0d6fe1a497ca041a4ad6265c597558595c3044eb17d696e34cad8fd7\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-build-statistics/2.3.10\",\n    \"dest-filename\": \"kotlin-build-statistics-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-statistics/2.3.10/kotlin-build-statistics-2.3.10.pom\",\n    \"sha512\": \"1b54450707f6c3001978ac809c3c924f75e9846042065644773a2b89e950cc9c431fcd602cc425627914fee523309d1a10e916e6809024d407bfdab6e1455baa\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-build-statistics/2.3.10\",\n    \"dest-filename\": \"kotlin-build-statistics-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-tools-api/2.3.10/kotlin-build-tools-api-2.3.10.jar\",\n    \"sha512\": \"6b8619c015fc3fc0e1668649358adead2916909f713e2be988a9799183e94a9ccbc5dc46610aef96da66e74eccfe77233719641e068d2f4f90d66d1ed6aae9d1\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-build-tools-api/2.3.10\",\n    \"dest-filename\": \"kotlin-build-tools-api-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-build-tools-api/2.3.10/kotlin-build-tools-api-2.3.10.pom\",\n    \"sha512\": \"b8b09e88d3ac6ec5b79cf8ca65a997b633ceb7065ac80b1a64e19d5016bb00baa674f2879384597cbf758211d4f5aa483136d5ccb6dba5bc2bcf2ddbbbdb79d3\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-build-tools-api/2.3.10\",\n    \"dest-filename\": \"kotlin-build-tools-api-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-runner/2.3.10/kotlin-compiler-runner-2.3.10.jar\",\n    \"sha512\": \"55add2f091f67c651cbdd7c912f58466f3e402893c64164aae51094a4f2eb063e020660ef7f9b0770b9bb61ced0829a1906226f4c82d4a54d376b9d56c49e3b3\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-compiler-runner/2.3.10\",\n    \"dest-filename\": \"kotlin-compiler-runner-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-compiler-runner/2.3.10/kotlin-compiler-runner-2.3.10.pom\",\n    \"sha512\": \"8c15301805ef93f0f637be11122fcec101e9927a40805b0534de303bb65f0403136fe458b285a972d042a9b99ad1fe48f8a5d1ae0d21cb4fa19df6829b0e3909\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-compiler-runner/2.3.10\",\n    \"dest-filename\": \"kotlin-compiler-runner-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-client/2.3.10/kotlin-daemon-client-2.3.10.jar\",\n    \"sha512\": \"74c66b696eaf9f5fabc6f18914154dbdd21c890294344e95638738a37845afaff2157e9ff72c024a02530a25c9f17ab5c3c8755124ff6ab02ce64eb76cc187fb\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-daemon-client/2.3.10\",\n    \"dest-filename\": \"kotlin-daemon-client-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-daemon-client/2.3.10/kotlin-daemon-client-2.3.10.pom\",\n    \"sha512\": \"b607b22a940465aa9448fbb19fdc2fe25e2033165b433cbdd83277aca4f14bd6089ac8cdeeb4c64993ca1f83957558f71f00d513bfdfdba39943b6b53dd4889c\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-daemon-client/2.3.10\",\n    \"dest-filename\": \"kotlin-daemon-client-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-dataframe/2.3.10/kotlin-dataframe-2.3.10.pom\",\n    \"sha512\": \"c5a470578fcd93fa70ed31adfe196c015de0ec3624bde17f115ab5ea43440536cd5e4cabfc751df37dbc3ab3236f9ca5f0b623028b370d3864cce184a7bd03f8\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-dataframe/2.3.10\",\n    \"dest-filename\": \"kotlin-dataframe-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-ecosystem-plugin/2.3.10/kotlin-gradle-ecosystem-plugin-2.3.10.pom\",\n    \"sha512\": \"40ec3b42077ceaaba0296427c86e248e7cfb545af0baef82b92a31c36ae9d55c33e31de72bf9573b20827c98aa0121c3fbe389d55296d11041ea11d765b32b65\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-ecosystem-plugin/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-ecosystem-plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-annotations/2.3.10/kotlin-gradle-plugin-annotations-2.3.10.jar\",\n    \"sha512\": \"3b4a7870824a4db7068492e0242b74301eb6eba2325a37e1688e5ea1e0d0995e1c515c47131816e7d48860bd69775ba793d84bbf42ccd9e48f4bd6295060dfc3\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-annotations/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-annotations-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-annotations/2.3.10/kotlin-gradle-plugin-annotations-2.3.10.pom\",\n    \"sha512\": \"980d4664e142d18e67d9aa10b12dd1aad208a4e37f39b02f87e0a93948d6d668c04021ef57ddfef23432ec65bbefa941f1bef06a6bfce0281a23e5b255c2f212\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-annotations/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-annotations-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-api/2.3.10/kotlin-gradle-plugin-api-2.3.10-gradle813.jar\",\n    \"sha512\": \"c735e3b15461fec4404c3fd380d5ba2a4c2dadb66d8de2cffbbb4c8977f92cb42d25c9036e5caed83e08f5287b885bfc42de38688851fad4e6dafee8ec73e6f9\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-api/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-api-2.3.10-gradle813.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-api/2.3.10/kotlin-gradle-plugin-api-2.3.10.module\",\n    \"sha512\": \"4c75521b872db0eb92072d0ef01266d7c89543f90b3efbdde64df1c9a776af49d9fe2660e122b338765bda64e162eac9088002c402b7a5c6a17e8315d0ef30d2\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-api/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-api-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-api/2.3.10/kotlin-gradle-plugin-api-2.3.10.pom\",\n    \"sha512\": \"6340032af1caa8d7d3e0244f1298b86cbabeb2e60825c0eccf6b8e54ec05f1d6d8ea60836ecd7753b65e85cca4189e5162d14d1321f1715934eb1ba4bcf58b28\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-api/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-api-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/2.3.10/kotlin-gradle-plugin-idea-proto-2.3.10.jar\",\n    \"sha512\": \"6393f837e7e07b84a9c6ac82110eb3cdfa230ef655f8044f9f23432e350b5a4d16813752219912e08c5cbf80d80b817e6d9bdfbf37827553347e576f8256560f\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-idea-proto-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/2.3.10/kotlin-gradle-plugin-idea-proto-2.3.10.pom\",\n    \"sha512\": \"3b4357875326dbcfda1cd8307d2bf6be6dcdbc20ac47d774b2b1fd1cb4905f127a50a228fa581842074bf286f64c3eb9d37a6d7c1a560cfd76630acf4418fe23\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-idea-proto/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-idea-proto-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/2.3.10/kotlin-gradle-plugin-idea-2.3.10.jar\",\n    \"sha512\": \"82a20b477b6105fa99e83cfeee8ba7a5ca41c34e78ccdffc18405c21a405c865e8c0bd4af246d1604e5c62ac4ff6be16fad8bc2f7f48bc41996203c96fd95ece\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-idea-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/2.3.10/kotlin-gradle-plugin-idea-2.3.10.module\",\n    \"sha512\": \"35b7719617e681d9c9cb1ad1555ce1eea9a979aeba1d30c28c29e93cd4b7e05b47decc4fc07da4660eb8342edf3f93e1499fc7a151c485e86e53848604fd8e06\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-idea-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/2.3.10/kotlin-gradle-plugin-idea-2.3.10.pom\",\n    \"sha512\": \"5210b0b355487174a4db75b84e30cfbe2013c13c24ba0d177c475ac02dd818bdd77a45d139c7ee880cc101f7d8a520743e6a6edae6437236f137663b6b031ccf\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin-idea/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-idea-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/2.3.10/kotlin-gradle-plugin-2.3.10-gradle813.jar\",\n    \"sha512\": \"437c8570de7193e7f53a83c760d0ce6abea7ddf2fe647a9098e26af0ba654c5f5709634a2160a2e775559a1ff353aac331cfa04161f77abaa67881e75b33ba70\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-2.3.10-gradle813.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/2.3.10/kotlin-gradle-plugin-2.3.10.module\",\n    \"sha512\": \"582111fc42190ea8a27bbd729cb494830c47e60692be42563ff32463b174140f02584a971edcdfee074914a074b0e19f7e53ac4d7c162449ff8ba35f8ef647c5\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugin/2.3.10/kotlin-gradle-plugin-2.3.10.pom\",\n    \"sha512\": \"9394bd57bc878dbb8c427507a47eeeffbb00badec2e87e26ce55020d4c8a17b216350193399a1dd2792458439439c0ce834b54bd5ed874550f9a44e35fb7d542\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugin/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugins-bom/2.3.10/kotlin-gradle-plugins-bom-2.3.10.module\",\n    \"sha512\": \"289f9d6407d8db850439aafb2a5868ee3618bdc4815e6e9a624880a5d7d9a29ed40394e7ffa0a4dddad5270dfd1bf422d07e71706ec045ffc208a49dfa47ade2\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugins-bom/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugins-bom-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-gradle-plugins-bom/2.3.10/kotlin-gradle-plugins-bom-2.3.10.pom\",\n    \"sha512\": \"d02e66297461c67311016054bcefc533bad158770892dee09ce8d9230813d42b259077693fe71cee30d6e45f1f2bf0cb04dc291e5147f159e1cc8c16c4b15e5f\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-gradle-plugins-bom/2.3.10\",\n    \"dest-filename\": \"kotlin-gradle-plugins-bom-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-api/2.3.10/kotlin-klib-commonizer-api-2.3.10.jar\",\n    \"sha512\": \"0738d61e9230467269076f1b46cdf49cb8df562eb0f6849c2f09a8ff90c724afacfbf5e4a89c25ca440f0e47135306fc8017492bfd954486cd84133cf197e071\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-klib-commonizer-api/2.3.10\",\n    \"dest-filename\": \"kotlin-klib-commonizer-api-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-klib-commonizer-api/2.3.10/kotlin-klib-commonizer-api-2.3.10.pom\",\n    \"sha512\": \"c2701aeef668c211be5cd75262821f54e3c8309f5630c78503e4f161dcc447d006b516245c781ebaa1e5ac3e8c7a68a93816fcf88e7fdc7d920d65cafe6ecbce\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-klib-commonizer-api/2.3.10\",\n    \"dest-filename\": \"kotlin-klib-commonizer-api-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-lombok/2.3.10/kotlin-lombok-2.3.10.pom\",\n    \"sha512\": \"63035a7be9b0329606d03f78a39c03039df637bc75e5cf127272303b1e7c3921129ccb138848a33dc1e106bda7f0de27cdb9e6e602b196c8e67afd57ab579eb1\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-lombok/2.3.10\",\n    \"dest-filename\": \"kotlin-lombok-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-native-utils/2.3.10/kotlin-native-utils-2.3.10.jar\",\n    \"sha512\": \"0e55f086d3a82ce4d2c2179768becf2a52a8a08a37d27dc2976ed5f20f9470272adbcc99f6e389cb69d2eafc86f6da3f65be9d336206ad5d2607b1553e00dcb5\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-native-utils/2.3.10\",\n    \"dest-filename\": \"kotlin-native-utils-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-native-utils/2.3.10/kotlin-native-utils-2.3.10.pom\",\n    \"sha512\": \"ae651f77a3296b9f22fa6d3d51c229fea1eb9300a77056ad2a956f5e2114ced6e68e53ed23da48b085b0472f27f413ab47dc74d0836baacdda3f9a271593e8ef\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-native-utils/2.3.10\",\n    \"dest-filename\": \"kotlin-native-utils-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-noarg/2.3.10/kotlin-noarg-2.3.10.pom\",\n    \"sha512\": \"b06872b0862023d7c6c64df8a6a8920c2867a8453fb91be82f9c84608a8dc00d2617ce7188a23b940097d827ae1f6bdbc24bceb5ea29f1dc408315d4e548221e\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-noarg/2.3.10\",\n    \"dest-filename\": \"kotlin-noarg-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-power-assert/2.3.10/kotlin-power-assert-2.3.10.pom\",\n    \"sha512\": \"f00bfe942d7899c4955c5d13a3d1ef4a14d294bc25c91a8dab3b52b2574303814c3e26bd2782849e1f08eb7ecbfd68aa90939164a9792128f2fe09a80d5cc3f8\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-power-assert/2.3.10\",\n    \"dest-filename\": \"kotlin-power-assert-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/2.0.21/kotlin-reflect-2.0.21.jar\",\n    \"sha512\": \"3b35fb5684bc7cad47a5c068b2671ae55cabe6ca16745277ac44ce0785b1384ad35444fbe10b68ca7c360ba9a86b0061a6101956b370e6c19bd8c85c9bc13dcd\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-reflect/2.0.21\",\n    \"dest-filename\": \"kotlin-reflect-2.0.21.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-reflect/2.0.21/kotlin-reflect-2.0.21.pom\",\n    \"sha512\": \"39be2e959d5c2e2e22445edcfb41b43497b2ad5edfc936081a6a72fcf8b9291571f5d3d6a7ef5d7eb30ddd2cff744966f68ddfea2da33120bf30326e12492425\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-reflect/2.0.21\",\n    \"dest-filename\": \"kotlin-reflect-2.0.21.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-sam-with-receiver/2.3.10/kotlin-sam-with-receiver-2.3.10.pom\",\n    \"sha512\": \"c7c9b6f9ec69c2f2342f6a795131dccd8e89419dcd6655785275e3cc4e8f92fe9cd9d81db5179e1993dbe8732a24f77712024a3858981d618d2379a8376ae714\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-sam-with-receiver/2.3.10\",\n    \"dest-filename\": \"kotlin-sam-with-receiver-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-serialization/2.3.10/kotlin-serialization-2.3.10-gradle813.jar\",\n    \"sha512\": \"b33beaba9579e1d23e5795fd65e4d322fbf4a1cd84b65763a11ab58894133a75281607844b14fd18e4d6b9963cff5b6d5d784342dda66372a408840a2fef60fa\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-serialization/2.3.10\",\n    \"dest-filename\": \"kotlin-serialization-2.3.10-gradle813.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-serialization/2.3.10/kotlin-serialization-2.3.10.module\",\n    \"sha512\": \"24e6ac17ae4f3e0a5c7b3d4e70c240a778af5be3ff5b1144482862d86b537f2fe89b6f0adcbfab417e77dd5ca1d2735e9d5e3e296ebfbd71f6e6af4da650f74f\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-serialization/2.3.10\",\n    \"dest-filename\": \"kotlin-serialization-2.3.10.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-serialization/2.3.10/kotlin-serialization-2.3.10.pom\",\n    \"sha512\": \"ce290ee2f27d77f3d722d7df8ea0d3eefbca899a75f11e814c93f1ffdf33b5e8b67e997d6672bfc78964efa515fa7e9e5076be02f78633d78597f2af29e1db86\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-serialization/2.3.10\",\n    \"dest-filename\": \"kotlin-serialization-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-common/2.0.21/kotlin-stdlib-common-2.0.21.pom\",\n    \"sha512\": \"85a2f145f964d2296694ba5ab03f116268267fcca5eaf1f63445b5bfb3478997057d57f8e60661979e7aa55fc028728ac3aa7a385a4ef7109e904f104c59308e\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-common/2.0.21\",\n    \"dest-filename\": \"kotlin-stdlib-common-2.0.21.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0.pom\",\n    \"sha512\": \"f18fd89c03d5abccf2eb2a8306044dc505e29a2bb97e2d8afdd102d7d8c42908494182daa6133e8dceb221c8bcb9a52f62c7a7232c6657abb9c91c77b2a65af7\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0\",\n    \"dest-filename\": \"kotlin-stdlib-jdk7-1.8.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.2.0/kotlin-stdlib-jdk7-2.2.0.jar\",\n    \"sha512\": \"f1b3e9cf0bb85f66d553550fc6c3dd6a0a6937276879b4deed2b2f0d9f18eb146e4b6dc96b28fe7c257a02f20b5a62cbc20b52d3d9f8a034a8ea12320453687f\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.2.0\",\n    \"dest-filename\": \"kotlin-stdlib-jdk7-2.2.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.2.0/kotlin-stdlib-jdk7-2.2.0.pom\",\n    \"sha512\": \"76252bb08774b7cd60e19090295730249fa2d9187bdede24c7b8682cbac53d78f6fc18e7ff38cdce990537a26d29394bd5c92d1e25034ebbc2a71c80667d0c08\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.2.0\",\n    \"dest-filename\": \"kotlin-stdlib-jdk7-2.2.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0.pom\",\n    \"sha512\": \"2c5b573c0e5fcd9137b614586f7e4b5becaff8edcd15431e785bce5014bd23e3c70f22be5fd95ab15ab99326fddc2b27fcdc9726312a59c4d47d34e88940d3f9\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0\",\n    \"dest-filename\": \"kotlin-stdlib-jdk8-1.8.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.2.0/kotlin-stdlib-jdk8-2.2.0.jar\",\n    \"sha512\": \"6307f0854f21811b0b5ff54ada950d44391a813fa41bf3c0de74ebee4d5ca6b861eaa1bec9e6aa991c7942ea0b67a6504adb8198dd5a42d86240d58465e29dfb\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.2.0\",\n    \"dest-filename\": \"kotlin-stdlib-jdk8-2.2.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.2.0/kotlin-stdlib-jdk8-2.2.0.pom\",\n    \"sha512\": \"4a49e2e4635c792e0f57f45e7f2463a95d2d9af92a8c62d1ca6f1460bc231afca5968e65667fa87e02a9dc129fa815ca2c1ba804e9780c2c17b2886a76790acd\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.2.0\",\n    \"dest-filename\": \"kotlin-stdlib-jdk8-2.2.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21.jar\",\n    \"sha512\": \"4e6786c4e7af13eddc5a2a06b336f430987f16ac1f4c0af541cd97400d689f7d8726a276ca549e7f19d7b939f47a1535dff7ea45f76b14197a0e876cc44d3acd\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib/2.0.21\",\n    \"dest-filename\": \"kotlin-stdlib-2.0.21.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21.module\",\n    \"sha512\": \"28d2618070b9a596ed01a547f360d21bee2d504020952cda8b8a914ee05454b6560eacb4d4d602ae61b915559e76acbb3bcc96b25fe76140ec14cca5d8c07261\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib/2.0.21\",\n    \"dest-filename\": \"kotlin-stdlib-2.0.21.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21.pom\",\n    \"sha512\": \"8b1f1ffcd1fdc8f71183f80fad469036a6bdfce169c5691484070259fc37d3eab7879e0658ef605e5609469a18c9c16f77f3123de1f0605153feff616985beb0\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-stdlib/2.0.21\",\n    \"dest-filename\": \"kotlin-stdlib-2.0.21.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-tooling-core/2.3.10/kotlin-tooling-core-2.3.10.jar\",\n    \"sha512\": \"831ec9092a896fefa8c4b2bd0c2304307dfe812ad83738e83870ae4de75d3d829cd89c576e9daadcda5c8d54777371bb5611482cfd9d6bab5fe1645dda7132a3\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-tooling-core/2.3.10\",\n    \"dest-filename\": \"kotlin-tooling-core-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-tooling-core/2.3.10/kotlin-tooling-core-2.3.10.pom\",\n    \"sha512\": \"deddb4bb53eea087d5037914a3e41284503617fe6c96d4d953c9d8722ae912901aa5c0cd3371157265221c7822b4980eb899709d7fc1d942ac9572b0f6d75472\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-tooling-core/2.3.10\",\n    \"dest-filename\": \"kotlin-tooling-core-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-io/2.3.10/kotlin-util-io-2.3.10.jar\",\n    \"sha512\": \"16d63d88531855789ba7a6d60ddf4af60f5d2fe89a08cbaa9e457a68ae3e0252d6247abc5a05a57ad0845d0820e4d6892b67d732c33b50a3a6c07f93f6cbff27\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-util-io/2.3.10\",\n    \"dest-filename\": \"kotlin-util-io-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-io/2.3.10/kotlin-util-io-2.3.10.pom\",\n    \"sha512\": \"930a58a550e65c320123b5ff30fade4788e9869e425b524d56f2b3a154175aa308fb68afc8cae04d2caf4d78cc0e1d2c9710629bb47c686906c4223582b95c66\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-util-io/2.3.10\",\n    \"dest-filename\": \"kotlin-util-io-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib-metadata/2.3.10/kotlin-util-klib-metadata-2.3.10.jar\",\n    \"sha512\": \"4c8d0e43d6ee8c76f9c007ad48ed1db93dc89cda7bf43d4015f689b48dc486cee4470b9f8b597dbae4562be14ee24826071eb328ba78d443b8d4821eab10c8a8\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-util-klib-metadata/2.3.10\",\n    \"dest-filename\": \"kotlin-util-klib-metadata-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib-metadata/2.3.10/kotlin-util-klib-metadata-2.3.10.pom\",\n    \"sha512\": \"ae9ca9cc720897b485fa63ff4a069c72f3e9855dfa120d8a07275b84ec44fd48e35c6dd387e904c8e17c15f53416af071d509423e4bd5eaf9e091fbd646784e9\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-util-klib-metadata/2.3.10\",\n    \"dest-filename\": \"kotlin-util-klib-metadata-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib/2.3.10/kotlin-util-klib-2.3.10.jar\",\n    \"sha512\": \"0db8e49cc4ec210543e074d5e806b95db6493137d0c89033fab57b3e1aa87f19cebed498c94c136e768a84957601bb6aaf99e6badc0caf66c58086d546de0ed2\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-util-klib/2.3.10\",\n    \"dest-filename\": \"kotlin-util-klib-2.3.10.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-util-klib/2.3.10/kotlin-util-klib-2.3.10.pom\",\n    \"sha512\": \"14c8ff493c14414dc9ceb893125d8026589bddcea04896dceaa4e91aad383f56270ce5e88aa8122adc81aa57ce3fed16a7b974d70c086dfe35b511103877fc58\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/kotlin-util-klib/2.3.10\",\n    \"dest-filename\": \"kotlin-util-klib-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/multiplatform/org.jetbrains.kotlin.multiplatform.gradle.plugin/2.3.10/org.jetbrains.kotlin.multiplatform.gradle.plugin-2.3.10.pom\",\n    \"sha512\": \"2a92c828efbfc35129490ef52821ba167bbf924f90122cb48419670de8d032e2b340828adb5de908538e0097c4bdb16245789fdcfd31b201ab89417826018f76\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/multiplatform/org.jetbrains.kotlin.multiplatform.gradle.plugin/2.3.10\",\n    \"dest-filename\": \"org.jetbrains.kotlin.multiplatform.gradle.plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/plugin/compose/org.jetbrains.kotlin.plugin.compose.gradle.plugin/2.3.10/org.jetbrains.kotlin.plugin.compose.gradle.plugin-2.3.10.pom\",\n    \"sha512\": \"6c555afbbcef56cd0c9040e9545ea168d1edf9bb947bf6e215ab2d54877f83499b257ebaa2c761b2148b2cd52ab4daa576fcc598a62a7985b6dab5753b1b7b40\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/plugin/compose/org.jetbrains.kotlin.plugin.compose.gradle.plugin/2.3.10\",\n    \"dest-filename\": \"org.jetbrains.kotlin.plugin.compose.gradle.plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/plugin/serialization/org.jetbrains.kotlin.plugin.serialization.gradle.plugin/2.3.10/org.jetbrains.kotlin.plugin.serialization.gradle.plugin-2.3.10.pom\",\n    \"sha512\": \"f48cfff9d983d82a6e1939bf432f79adca26f51a6d1bae72110a3102d05e88590e2dcfd98722ca7c03f103926ff2ce196cbffd8ddf6f5bf485b3aaaadf4d4c8b\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlin/plugin/serialization/org.jetbrains.kotlin.plugin.serialization.gradle.plugin/2.3.10\",\n    \"dest-filename\": \"org.jetbrains.kotlin.plugin.serialization.gradle.plugin-2.3.10.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.9.0/kotlinx-coroutines-android-1.9.0.pom\",\n    \"sha512\": \"f072c0f0b90ff949580b5d0c195f5051cb7e5245ee8fa4fad2c49e4f8a8344521ff2ba3d113342aa0bc7a40587f28dffe4d0375c17f9d659d5189c4b4c94e6c4\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-android-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.9.0/kotlinx-coroutines-bom-1.9.0.pom\",\n    \"sha512\": \"6d7f6ee873c622ac45c12266c1b2dc7086cb7273a77215ef9c51ce3b8cc1ac3415229b193792d1900d0ed9fba3f464eaa380741e813ed4ed364a6bc21de17671\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-bom/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-bom-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0.jar\",\n    \"sha512\": \"78c77268ec81ee580c9481ae35b667912e186e11576acc669ad4e8c17041506db5ba8adae439b50685a7cedf58755f0df7f2aa82ad1350e499431b13e6752a1a\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-core-jvm-1.9.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0.module\",\n    \"sha512\": \"2a2ddb002d60f0a4bc757b8deacd2a89b1a86c6601bd52911af701728cc0b796e3ae743b4f2b847036eeb8ce58141a0f438ee600ba198cd55e95031af7081e4e\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-core-jvm-1.9.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0.pom\",\n    \"sha512\": \"d04fca8b3b465e781f48b7903ff8d880ca305a66c038e02da51ca87a0ee5c96275f911c673d779a364e82a325e427f4c11f5f83ed3c815cd75f6291fb96c0625\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-core-jvm-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0/../../kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0.module\",\n    \"sha512\": \"2a2ddb002d60f0a4bc757b8deacd2a89b1a86c6601bd52911af701728cc0b796e3ae743b4f2b847036eeb8ce58141a0f438ee600ba198cd55e95031af7081e4e\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0/../../kotlinx-coroutines-core-jvm/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-core-jvm-1.9.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0/kotlinx-coroutines-core-1.9.0.module\",\n    \"sha512\": \"e41d384e7e11561a243f838c49bbdf031354ebf51690cfb23aba2e734c8220729ac1b339938853c4b658f0440272fbe09465e17be46e231e21898a9d4fbdc84c\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-core-1.9.0.module\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0/kotlinx-coroutines-core-1.9.0.pom\",\n    \"sha512\": \"a0031bc41d95c457e2be4ba52dd7af151e5b7519cf01591ea258b7654054666f98f1f82911e4d8282e4d574db3dfffab917031d4e5493acf6df006469f24dbb5\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-core-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-debug/1.9.0/kotlinx-coroutines-debug-1.9.0.pom\",\n    \"sha512\": \"dee256f7eabe987e69ed8065143f84467df166fb8ed9b7c9a1f86461752826e5561bd90aa6065106374eaf6e8d4f77d8567828b119a5cc7df919acf57d0d8261\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-debug/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-debug-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-guava/1.9.0/kotlinx-coroutines-guava-1.9.0.pom\",\n    \"sha512\": \"7d2eb5891bc73fc89dccaf75aa19f75662c79c4c00e79d15aa9908df7c845e2748ad00561766ec4344ee5d8fa2fae6bf58fbee00042fca0ade4974d683e43afa\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-guava/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-guava-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-javafx/1.9.0/kotlinx-coroutines-javafx-1.9.0.pom\",\n    \"sha512\": \"841f9e626bca051af2b1c6929b79580dc22b1d8ba3387316b5ae058f28d3d3e481089b517743f0d41add210e11acad7b8b43d4dc1f05e4a9102f0b3b37cfb0cc\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-javafx/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-javafx-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-jdk8/1.9.0/kotlinx-coroutines-jdk8-1.9.0.pom\",\n    \"sha512\": \"9c857e4f76314b851de229255107e553b30f8f7e166baadb97c81d436bef07021d7e22f821842122417dd33030fea30a7b5bff31de7cdac354dfefdd27b05e2e\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-jdk8/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-jdk8-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-jdk9/1.9.0/kotlinx-coroutines-jdk9-1.9.0.pom\",\n    \"sha512\": \"55d85bd1b545e14f15bd15ec33ac6124527c5d5bb4b155de3989b49562998147f0a9d284271b22013c48c26e4da197841646d5bd3297416477182c9c1861dbda\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-jdk9/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-jdk9-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-play-services/1.9.0/kotlinx-coroutines-play-services-1.9.0.pom\",\n    \"sha512\": \"62209ffa7b931c247ac908cc26f23848206ee04d9641c67a44041cf6721bc36abc6fb49a9ebdcfa537063d88e1820e9d693c0ee96d916b6e8f329a95487c4bed\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-play-services/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-play-services-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-reactive/1.9.0/kotlinx-coroutines-reactive-1.9.0.pom\",\n    \"sha512\": \"626c539907c593900b7ef87e65aace18715023c88069a55e23ae2cfa057172c8b0d46ba5fdd37556cfad5b40237bcbce84ec1bac3578ab3acebcaee6d4c2830f\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-reactive/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-reactive-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-reactor/1.9.0/kotlinx-coroutines-reactor-1.9.0.pom\",\n    \"sha512\": \"3c1e81de7177dda2b7f6b456b90c7243d5b9dfa8b337b8c489d0ac859bcc45d1a585db2f3b0938c59c5bed55e890c8b8a69b467b3250792a1076811f38b1ceb8\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-reactor/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-reactor-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-rx2/1.9.0/kotlinx-coroutines-rx2-1.9.0.pom\",\n    \"sha512\": \"f73b1a9c4176e4413f028154b1dfbd773eea996df96b49b05eb107db0a3bd2633663283db423aa186a64b048b1a55374fe44279873e435137b4827768b50e78c\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-rx2/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-rx2-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-rx3/1.9.0/kotlinx-coroutines-rx3-1.9.0.pom\",\n    \"sha512\": \"10593151609dbf97648b401f18e44bedd14503bcccbe648d5798c9f19ee31950d815ceffe0db04766636dd63e96a5da4a2b0b79162b6446a9459075dde28b2ed\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-rx3/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-rx3-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-slf4j/1.9.0/kotlinx-coroutines-slf4j-1.9.0.pom\",\n    \"sha512\": \"6d789ed372c11771615f12b88a63aefaf4c5b812908b0a3f30a4a2ba6d0e4dd3490012c7db4c269b3df53bcab0fca75b25abed3719625d677320dd61a16fdaa9\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-slf4j/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-slf4j-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-swing/1.9.0/kotlinx-coroutines-swing-1.9.0.pom\",\n    \"sha512\": \"a286882296c08d110bf0fdbc6016416e9d91752e74d2840f4418417869380b1a6e1b46c7aa6db847ff19feadffc90104b486e5bb3dc32636ebd7940031acc0e8\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-swing/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-swing-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-test-jvm/1.9.0/kotlinx-coroutines-test-jvm-1.9.0.pom\",\n    \"sha512\": \"d5217ec7dde2d2b8553e4b455ce0bd3bcbae64100cd0d8f93f1754e90d770db56b9d81ff5b2abc3e973fab6ebce195e364714c4d8adc47cb2ff7b70e89f51152\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-test-jvm/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-test-jvm-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-test/1.9.0/kotlinx-coroutines-test-1.9.0.pom\",\n    \"sha512\": \"05fe09d332ba7df1238d79f839b2f9c28f24c4ec546a3a84f930ba524c3905c66c87be97f4b5ef33f6f0f83fe7f522b5d75dc4755cd8e9b70e95ec0a32d72f14\",\n    \"dest\": \"offline-repository/org/jetbrains/kotlinx/kotlinx-coroutines-test/1.9.0\",\n    \"dest-filename\": \"kotlinx-coroutines-test-1.9.0.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/junit/junit-bom/5.10.2/junit-bom-5.10.2.pom\",\n    \"sha512\": \"5b36199c9a331c4d05ed78b8fe1f28c5d21e0d7a05bdca1d45f532a89eb7d67fd425da93e9405203a483354abc63718858f80c828e0c090316f70fd833ef904c\",\n    \"dest\": \"offline-repository/org/junit/junit-bom/5.10.2\",\n    \"dest-filename\": \"junit-bom-5.10.2.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jvnet/staxex/stax-ex/1.8.1/stax-ex-1.8.1.jar\",\n    \"sha512\": \"d060ea5dcc81508a1078bd0ed947553a5652a5a78da76cd00dc2e384dca014166dfe92777fceb7d7868699894d7e40ecef99fac7771372025bfd7f1fa3e2fe32\",\n    \"dest\": \"offline-repository/org/jvnet/staxex/stax-ex/1.8.1\",\n    \"dest-filename\": \"stax-ex-1.8.1.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/jvnet/staxex/stax-ex/1.8.1/stax-ex-1.8.1.pom\",\n    \"sha512\": \"430d30c5de912c6025635e03741451f1735cb0362327d9d2cf8c8612da5a265cb25f05015ef9156480966c41cf84b62342f921dcef65a27af0c1038dea0cd850\",\n    \"dest\": \"offline-repository/org/jvnet/staxex/stax-ex/1.8.1\",\n    \"dest-filename\": \"stax-ex-1.8.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/9.8/asm-analysis-9.8.jar\",\n    \"sha512\": \"0268e6dc2cc4965180ca1b62372e3c5fc280d6dc09cfeace2ac4e43468025e8a78813e4e93beafc0352e67498c70616cb4368313aaab532025fa98146c736117\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-analysis/9.8\",\n    \"dest-filename\": \"asm-analysis-9.8.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-analysis/9.8/asm-analysis-9.8.pom\",\n    \"sha512\": \"7b65663a02d30ac3c499cd63d8b057091a394a73f5187c33f325c4f6dfdf103940ba29e1d02c3f59e0cb262b3015daf5dcb058ff87e0a08762071e47a1f0ffec\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-analysis/9.8\",\n    \"dest-filename\": \"asm-analysis-9.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/9.8/asm-commons-9.8.jar\",\n    \"sha512\": \"d2add10e25416b701bd84651b42161e090df2f32940de5e06e0e2a41c6106734db2fe5136f661d8a8af55e80dc958bc7b385a1004f0ebe550828dfa1e9d70d41\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-commons/9.8\",\n    \"dest-filename\": \"asm-commons-9.8.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-commons/9.8/asm-commons-9.8.pom\",\n    \"sha512\": \"a5137b1ead49660b104f0e8d54b9df646d0f87645905c38e7df0b6d10d7f8e0aabe43db227dc005fd63765eded2554c23fd0495cc25cdf4112447bd3300d9bae\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-commons/9.8\",\n    \"dest-filename\": \"asm-commons-9.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/9.8/asm-tree-9.8.jar\",\n    \"sha512\": \"4493f573d9f0cfc8837db9be25a8b61a825a06aafc0e02f0363875584ff184a5a14600e53793c09866300859e44f153faffd0e050de4a7fba1a63b5fb010a9a7\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-tree/9.8\",\n    \"dest-filename\": \"asm-tree-9.8.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-tree/9.8/asm-tree-9.8.pom\",\n    \"sha512\": \"db2073ff628d0a146bf4c132fc900d9a646476fdeda35cee7f1f0d2a486848d4cbc77fd28bf2a28bd9855e54ec3b8114d2961cee580dd27ce7d48f4ebf2821be\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-tree/9.8\",\n    \"dest-filename\": \"asm-tree-9.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/9.8/asm-util-9.8.jar\",\n    \"sha512\": \"b68048e199c49d2f90b2990c6993f1fcddccd34fb9d91154ef327d874aa5ff8609db5fbd63e23141020cdeda8fb753e97a61c2152e1b4e8f20003a5390e7e1d9\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-util/9.8\",\n    \"dest-filename\": \"asm-util-9.8.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm-util/9.8/asm-util-9.8.pom\",\n    \"sha512\": \"281e96ee5a2bffc3cd4af80bab993a57ad2833769cd4c3941fa8709a9d552ab36c4336e85dfebd5e98aa7eaceb647211f8d5938f11fdd54edd846a2e8013523c\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm-util/9.8\",\n    \"dest-filename\": \"asm-util-9.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm/9.8/asm-9.8.jar\",\n    \"sha512\": \"cbd250b9c698a48a835e655f5f5262952cc6dd1a434ec0bc3429a9de41f2ce08fcd3c4f569daa7d50321ca6ad1d32e131e4199aa4fe54bce9e9691b37e45060e\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm/9.8\",\n    \"dest-filename\": \"asm-9.8.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/asm/asm/9.8/asm-9.8.pom\",\n    \"sha512\": \"a34a1cc4ac50724e0798f4d8ee677207ba68e4f311d9d5cc200dd9351ea63f556589aa42c7a0df90b53634ae41bae3646d5115eb029aab11077a20f7b144bf3f\",\n    \"dest\": \"offline-repository/org/ow2/asm/asm/9.8\",\n    \"dest-filename\": \"asm-9.8.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/ow2/ow2/1.5.1/ow2-1.5.1.pom\",\n    \"sha512\": \"5dbdf60bace26f9dbe2610d3de178e729fae77d65f57cb8238a828d020aaf1b4cc3d3d804bbdcf4a385c141b14fbf92ff689d9caa1f9e86542b5c47b0b1e9288\",\n    \"dest\": \"offline-repository/org/ow2/ow2/1.5.1\",\n    \"dest-filename\": \"ow2-1.5.1.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar\",\n    \"sha512\": \"e5435852569dda596ba46138af8ee9c4ecba8a7a43f4f1e7897aeb4430523a0f037088a7b63877df5734578f19d331f03d7b0f32d5ae6c425df211947b3e6173\",\n    \"dest\": \"offline-repository/org/slf4j/slf4j-api/1.7.30\",\n    \"dest-filename\": \"slf4j-api-1.7.30.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.pom\",\n    \"sha512\": \"9a529f73f5409940553054d5f8ff5394ffe50ab772ef6fc9f052f8eb6bd64c3bfa84b615d3257f10b6dee85df0c340194616368b67bb16918af59f4770d38acf\",\n    \"dest\": \"offline-repository/org/slf4j/slf4j-api/1.7.30\",\n    \"dest-filename\": \"slf4j-api-1.7.30.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-parent/1.7.30/slf4j-parent-1.7.30.pom\",\n    \"sha512\": \"1103234e739366ed3a4a82451723b6a495243ced75c73cf4a9fddd36381a11ecd7f454f5abc7eab13b2f5d594db9192831483aee7a6e26cbd6e4d0cd1eae260d\",\n    \"dest\": \"offline-repository/org/slf4j/slf4j-parent/1.7.30\",\n    \"dest-filename\": \"slf4j-parent-1.7.30.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/sonatype/oss/oss-parent/7/oss-parent-7.pom\",\n    \"sha512\": \"63b0951f793ee9d25239ee44760e4d51de3b8503e438e567862306f2d175019d8617eb854bc4ee2374c39f385e0a1094c3c7097f899b2074e4acda14fe6030fb\",\n    \"dest\": \"offline-repository/org/sonatype/oss/oss-parent/7\",\n    \"dest-filename\": \"oss-parent-7.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/sonatype/oss/oss-parent/9/oss-parent-9.pom\",\n    \"sha512\": \"1c4f18cacd3a9f99a168bd20845d12d94824e66b5caebe57c164b6ac3dc89803508f60e35c4d1da81d3f3866c89ed407826f0d108f1e624c2d8a258e01e1064a\",\n    \"dest\": \"offline-repository/org/sonatype/oss/oss-parent/9\",\n    \"dest-filename\": \"oss-parent-9.pom\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/tensorflow/tensorflow-lite-metadata/0.2.0/tensorflow-lite-metadata-0.2.0.jar\",\n    \"sha512\": \"b09be234ced89c7f6bdcb77af540cc4cd2dfaa3b88a024897add2b79fe925740a42ab7395e01c2bc91228d7bb8679553502cccf07f1a1cd2fa2ddd580ec5d160\",\n    \"dest\": \"offline-repository/org/tensorflow/tensorflow-lite-metadata/0.2.0\",\n    \"dest-filename\": \"tensorflow-lite-metadata-0.2.0.jar\"\n  },\n  {\n    \"type\": \"file\",\n    \"url\": \"https://repo.maven.apache.org/maven2/org/tensorflow/tensorflow-lite-metadata/0.2.0/tensorflow-lite-metadata-0.2.0.pom\",\n    \"sha512\": \"54bbfd5a6f9ff7db32d12c2f4dee9769bdb23bad1bebec0c705b79a7a324cdeb51a53c75c03328462272ffff0ea9602ac64bb3ab3649d6df742a2bb4b89e8e55\",\n    \"dest\": \"offline-repository/org/tensorflow/tensorflow-lite-metadata/0.2.0\",\n    \"dest-filename\": \"tensorflow-lite-metadata-0.2.0.pom\"\n  }\n]\n"
  },
  {
    "path": "packaging/flatpak/githubstore.sh",
    "content": "#!/bin/bash\n# GitHub Store Flatpak launcher script\n# Launches the uber JAR with the bundled JetBrains Runtime\n\nexport JAVA_HOME=/app/jre\n\n# Ensure config directory exists\nmkdir -p \"${XDG_CONFIG_HOME:-$HOME/.config}/githubstore\"\nmkdir -p \"${XDG_DATA_HOME:-$HOME/.local/share}/githubstore\"\n\nexec /app/jre/bin/java \\\n    -Djava.awt.headless=false \\\n    -Dawt.useSystemAAFontSettings=on \\\n    -Dswing.aatext=true \\\n    -Djava.util.prefs.userRoot=\"${XDG_CONFIG_HOME:-$HOME/.config}/githubstore\" \\\n    -Dapp.data.dir=\"${XDG_DATA_HOME:-$HOME/.local/share}/githubstore\" \\\n    -Dapp.downloads.dir=\"${XDG_DOWNLOAD_DIR:-$HOME/Downloads}\" \\\n    -jar /app/lib/githubstore.jar \"$@\"\n"
  },
  {
    "path": "packaging/flatpak/zed.rainxch.githubstore.desktop",
    "content": "[Desktop Entry]\nType=Application\nName=GitHub Store\nGenericName=App Store for GitHub Releases\nComment=Browse, download, and manage apps from GitHub releases\nIcon=zed.rainxch.githubstore\nExec=githubstore %u\nTerminal=false\nStartupNotify=true\nCategories=Development;PackageManager;\nKeywords=GitHub;Apps;Releases;APK;Store;\nStartupWMClass=GitHub-Store\nMimeType=x-scheme-handler/githubstore;\n"
  },
  {
    "path": "packaging/flatpak/zed.rainxch.githubstore.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop-application\">\n  <id>zed.rainxch.githubstore</id>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>Apache-2.0</project_license>\n\n  <name>GitHub Store</name>\n  <summary>A cross-platform app store for GitHub releases</summary>\n\n  <description>\n    <p>\n      GitHub Store lets you browse, download, and manage applications distributed\n      through GitHub releases. It brings the convenience of an app store to the\n      open-source ecosystem.\n    </p>\n    <p>Features include:</p>\n    <ul>\n      <li>Discover trending, hot, and popular repositories with installable releases</li>\n      <li>Search and filter repositories by language, topic, and more</li>\n      <li>View release notes, changelogs, and repository READMEs with translation support</li>\n      <li>Secure installs with TOFU signing key verification and provenance attestation checks</li>\n      <li>Save favourites and starred repositories for quick access</li>\n      <li>GitHub OAuth authentication for higher API rate limits and personalized experience</li>\n      <li>Material 3 theming with light/dark mode and 12 language translations</li>\n    </ul>\n  </description>\n\n  <launchable type=\"desktop-id\">zed.rainxch.githubstore.desktop</launchable>\n\n  <screenshots>\n    <screenshot type=\"default\">\n      <caption>Home screen showing trending repositories</caption>\n      <image>https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/main/media-resources/screenshots/desktop/desktop_linux_home.jpg</image>\n    </screenshot>\n    <screenshot>\n      <caption>Repository details with release info</caption>\n      <image>https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/main/media-resources/screenshots/desktop/desktop_linux_details.jpg</image>\n    </screenshot>\n    <screenshot>\n      <caption>Installing packages on Linux</caption>\n      <image>https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/main/media-resources/screenshots/desktop/desktop_linux_installing_rpm.jpg</image>\n    </screenshot>\n    <screenshot>\n      <caption>AppImage support on Linux</caption>\n      <image>https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/main/media-resources/screenshots/desktop/desktop_linux_appimage.jpg</image>\n    </screenshot>\n  </screenshots>\n\n  <url type=\"homepage\">https://github.com/OpenHub-Store/GitHub-Store</url>\n  <url type=\"bugtracker\">https://github.com/OpenHub-Store/GitHub-Store/issues</url>\n  <url type=\"vcs-browser\">https://github.com/OpenHub-Store/GitHub-Store</url>\n\n  <developer id=\"zed.rainxch\">\n    <name>rainxchzed</name>\n  </developer>\n\n  <content_rating type=\"oars-1.1\"/>\n\n  <releases>\n    <release version=\"1.6.2\" date=\"2026-03-18\">\n      <description>\n        <p>Security improvements: package name and signing key validation on updates,\n           provenance attestation badges, and app-link verification with asset picker.</p>\n      </description>\n    </release>\n  </releases>\n\n  <categories>\n    <category>Development</category>\n    <category>PackageManager</category>\n  </categories>\n\n  <keywords>\n    <keyword>GitHub</keyword>\n    <keyword>releases</keyword>\n    <keyword>app store</keyword>\n    <keyword>package manager</keyword>\n    <keyword>APK</keyword>\n  </keywords>\n\n  <supports>\n    <control>pointing</control>\n    <control>keyboard</control>\n  </supports>\n\n  <requires>\n    <display_length compare=\"ge\">768</display_length>\n  </requires>\n</component>\n"
  },
  {
    "path": "packaging/flatpak/zed.rainxch.githubstore.yml",
    "content": "app-id: zed.rainxch.githubstore\nruntime: org.freedesktop.Platform\nruntime-version: '24.08'\nsdk: org.freedesktop.Sdk\nsdk-extensions:\n  - org.freedesktop.Sdk.Extension.openjdk21\ncommand: githubstore\n\nfinish-args:\n  # Network access for GitHub API calls and downloading releases\n  - --share=network\n  # X11 display support\n  - --share=ipc\n  - --socket=x11\n  # GPU acceleration for Compose rendering\n  - --device=dri\n  # Download directory access for APK/release downloads\n  - --filesystem=xdg-download:rw\n  # Wayland support (fallback to X11 via XWayland)\n  - --socket=fallback-x11\n  - --socket=wayland\n\nmodules:\n  # Module 1: Bundle JetBrains Runtime (JBR) 21 for optimal Compose Desktop rendering\n  # Using plain jbr (no JCEF) — app doesn't use embedded Chromium\n  - name: jbr\n    buildsystem: simple\n    build-commands:\n      - mkdir -p /app/jre\n      - tar xzf jbr-*.tar.gz -C /app/jre --strip-components=1\n    sources:\n      # x86_64\n      - type: file\n        url: https://cache-redirector.jetbrains.com/intellij-jbr/jbr-21.0.10-linux-x64-b1163.105.tar.gz\n        sha256: b6a3b13451d296140727bbde9325dd9ee422e2e9b2c6a9378f346f1b8d1111bc\n        only-arches:\n          - x86_64\n      # aarch64\n      - type: file\n        url: https://cache-redirector.jetbrains.com/intellij-jbr/jbr-21.0.10-linux-aarch64-b1163.105.tar.gz\n        sha256: 38804f526d869f5a9c49e9b90c04edcbc918b41e8e43264e5d5076d352a959bc\n        only-arches:\n          - aarch64\n\n  # Module 2: Build and install GitHub Store\n  - name: githubstore\n    buildsystem: simple\n    build-options:\n      append-path: /usr/lib/sdk/openjdk21/bin\n      env:\n        JAVA_HOME: /usr/lib/sdk/openjdk21\n        GRADLE_USER_HOME: /run/build/githubstore/.gradle\n    build-commands:\n      # Use local Gradle distribution (no network in sandbox)\n      - sed -i 's|distributionUrl=.*|distributionUrl=gradle-bin.zip|' gradle/wrapper/gradle-wrapper.properties\n      # Disable Android targets (no Android SDK in Flatpak sandbox)\n      - bash packaging/flatpak/disable-android-for-flatpak.sh\n      # Build uber JAR\n      - ./gradlew :composeApp:packageReleaseUberJarForCurrentOS\n          --no-daemon\n          --offline\n          --no-configuration-cache\n          -Dorg.gradle.jvmargs=\"-Xmx6g -XX:MaxMetaspaceSize=2g\"\n          -Dorg.gradle.parallel=false\n      # Install the JAR\n      - install -Dm644 \"$(find composeApp/build/compose/jars/ -name '*.jar' -type f | head -1)\"\n          /app/lib/githubstore.jar\n      # Install launcher script\n      - install -Dm755 packaging/flatpak/githubstore.sh /app/bin/githubstore\n      # Install desktop entry\n      - install -Dm644 packaging/flatpak/zed.rainxch.githubstore.desktop\n          /app/share/applications/zed.rainxch.githubstore.desktop\n      # Install AppStream metainfo\n      - install -Dm644 packaging/flatpak/zed.rainxch.githubstore.metainfo.xml\n          /app/share/metainfo/zed.rainxch.githubstore.metainfo.xml\n      # Install icon (512x512)\n      - install -Dm644 composeApp/src/jvmMain/resources/logo/app_icon.png\n          /app/share/icons/hicolor/512x512/apps/zed.rainxch.githubstore.png\n    sources:\n      - type: git\n        url: https://github.com/OpenHub-Store/GitHub-Store\n        tag: v1.6.2\n        # commit: REPLACE_WITH_COMMIT_HASH\n      # Local Gradle distribution (pre-downloaded, no network in sandbox)\n      - type: file\n        url: https://services.gradle.org/distributions/gradle-8.14.3-bin.zip\n        sha256: bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531\n        dest: gradle/wrapper\n        dest-filename: gradle-bin.zip\n      # Pre-downloaded Maven/Gradle dependencies\n      # Generate with: ./gradlew flatpakGradleGenerator\n      - flatpak-sources.json\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "rootProject.name = \"GithubStore\"\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n\npluginManagement {\n    includeBuild(\"build-logic\")\n\n    repositories {\n        google {\n            mavenContent {\n                includeGroupAndSubgroups(\"androidx\")\n                includeGroupAndSubgroups(\"com.android\")\n                includeGroupAndSubgroups(\"com.google\")\n            }\n        }\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\ndependencyResolutionManagement {\n    repositories {\n        google {\n            mavenContent {\n                includeGroupAndSubgroups(\"androidx\")\n                includeGroupAndSubgroups(\"com.android\")\n                includeGroupAndSubgroups(\"com.google\")\n            }\n        }\n        mavenCentral()\n    }\n}\n\nplugins {\n    id(\"org.gradle.toolchains.foojay-resolver-convention\") version \"1.0.0\"\n}\n\ninclude(\":composeApp\")\ninclude(\":core:domain\")\ninclude(\":core:data\")\ninclude(\":core:presentation\")\ninclude(\":feature:apps:data\")\ninclude(\":feature:apps:domain\")\ninclude(\":feature:apps:presentation\")\ninclude(\":feature:auth:domain\")\ninclude(\":feature:auth:data\")\ninclude(\":feature:auth:presentation\")\ninclude(\":feature:details:domain\")\ninclude(\":feature:details:data\")\ninclude(\":feature:details:presentation\")\ninclude(\":feature:dev-profile:presentation\")\ninclude(\":feature:dev-profile:data\")\ninclude(\":feature:dev-profile:domain\")\ninclude(\":feature:favourites:data\")\ninclude(\":feature:favourites:domain\")\ninclude(\":feature:favourites:presentation\")\ninclude(\":feature:home:domain\")\ninclude(\":feature:home:data\")\ninclude(\":feature:home:presentation\")\ninclude(\":feature:starred:domain\")\ninclude(\":feature:starred:data\")\ninclude(\":feature:starred:presentation\")\ninclude(\":feature:search:domain\")\ninclude(\":feature:search:data\")\ninclude(\":feature:search:presentation\")\ninclude(\":feature:profile:domain\")\ninclude(\":feature:profile:data\")\ninclude(\":feature:profile:presentation\")\n"
  }
]